Merge branch 'beta' into tera-starter-choice

This commit is contained in:
Xavion3 2025-02-20 23:37:36 +11:00
commit b4a6bab9b2
18 changed files with 850 additions and 84 deletions

View File

@ -1044,9 +1044,9 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)
&& !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
const tempAbilityId = attacker.getAbility().id;
attacker.summonData.ability = pokemon.getAbility().id;
pokemon.summonData.ability = tempAbilityId;
const tempAbility = attacker.getAbility();
attacker.setTempAbility(pokemon.getAbility());
pokemon.setTempAbility(tempAbility);
}
return true;
}
@ -1071,7 +1071,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr)
&& !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon)) {
if (!simulated) {
attacker.summonData.ability = this.ability;
attacker.setTempAbility(allAbilities[this.ability]);
}
return true;
@ -1908,8 +1908,8 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr {
applyPostKnockOut(pokemon: Pokemon, passive: boolean, simulated: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise<boolean> {
if (pokemon.isPlayer() === knockedOut.isPlayer() && !knockedOut.getAbility().hasAttr(UncopiableAbilityAbAttr)) {
if (!simulated) {
pokemon.summonData.ability = knockedOut.getAbility().id;
globalScene.queueMessage(i18next.t("abilityTriggers:copyFaintedAllyAbility", { pokemonNameWithAffix: getPokemonNameWithAffix(knockedOut), abilityName: allAbilities[knockedOut.getAbility().id].name }));
pokemon.setTempAbility(knockedOut.getAbility());
}
return true;
}
@ -1993,6 +1993,21 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
* @see {@linkcode applyPostSummon()}
*/
export class PostSummonAbAttr extends AbAttr {
/** Should the ability activate when gained in battle? This will almost always be true */
private activateOnGain: boolean;
constructor(showAbility: boolean = true, activateOnGain: boolean = true) {
super(showAbility);
this.activateOnGain = activateOnGain;
}
/**
* @returns Whether the ability should activate when gained in battle
*/
shouldActivateOnGain(): boolean {
return this.activateOnGain;
}
/**
* Applies ability post summon (after switching in)
* @param pokemon {@linkcode Pokemon} with this ability
@ -2330,7 +2345,7 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
if (!simulated) {
this.target = target!;
this.targetAbilityName = allAbilities[target!.getAbility().id].name;
pokemon.summonData.ability = target!.getAbility().id;
pokemon.setTempAbility(target!.getAbility());
setAbilityRevealed(target!);
pokemon.updateInfo();
}
@ -2427,7 +2442,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
*/
export class PostSummonTransformAbAttr extends PostSummonAbAttr {
constructor() {
super(true);
super(true, false);
}
async applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): Promise<boolean> {
@ -2462,7 +2477,6 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
}
pokemon.summonData.speciesForm = target.getSpeciesForm();
pokemon.summonData.ability = target.getAbility().id;
pokemon.summonData.gender = target.getGender();
// Copy all stats (except HP)
@ -2492,6 +2506,8 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
promises.push(pokemon.loadAssets(false).then(() => {
pokemon.playAnim();
pokemon.updateInfo();
// If the new ability activates immediately, it needs to happen after all the transform animations
pokemon.setTempAbility(target.getAbility());
}));
await Promise.all(promises);
@ -4852,53 +4868,72 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
showAbilityInstant: boolean = false,
simulated: boolean = false,
messages: string[] = [],
gainedMidTurn: boolean = false
) {
for (const passive of [ false, true ]) {
if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) {
if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id))) {
continue;
}
const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
for (const attr of ability.getAttrs(attrType)) {
const condition = attr.getCondition();
if (condition && !condition(pokemon)) {
continue;
}
globalScene.setPhaseQueueSplice();
let result = applyFunc(attr, passive);
// TODO Remove this when promises get reworked
if (result instanceof Promise) {
result = await result;
}
if (result) {
if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) {
pokemon.summonData.abilitiesApplied.push(ability.id);
}
if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) {
pokemon.battleData.abilitiesApplied.push(ability.id);
}
if (attr.showAbility && !simulated) {
if (showAbilityInstant) {
globalScene.abilityBar.showAbility(pokemon, passive);
} else {
queueShowAbility(pokemon, passive);
}
}
const message = attr.getTriggerMessage(pokemon, ability.name, args);
if (message) {
if (!simulated) {
globalScene.queueMessage(message);
}
}
messages.push(message!);
}
}
applySingleAbAttrs(pokemon, passive, attrType, applyFunc, args, gainedMidTurn, simulated, showAbilityInstant, messages);
globalScene.clearPhaseQueueSplice();
}
}
async function applySingleAbAttrs<TAttr extends AbAttr>(
pokemon: Pokemon,
passive: boolean,
attrType: Constructor<TAttr>,
applyFunc: AbAttrApplyFunc<TAttr>,
args: any[],
gainedMidTurn: boolean = false,
simulated: boolean = false,
showAbilityInstant: boolean = false,
messages: string[] = []
) {
const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
if (gainedMidTurn && ability.getAttrs(attrType).some(attr => attr instanceof PostSummonAbAttr && !attr.shouldActivateOnGain())) {
return;
}
for (const attr of ability.getAttrs(attrType)) {
const condition = attr.getCondition();
if (condition && !condition(pokemon)) {
continue;
}
globalScene.setPhaseQueueSplice();
let result = applyFunc(attr, passive);
// TODO Remove this when promises get reworked
if (result instanceof Promise) {
result = await result;
}
if (result) {
if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) {
pokemon.summonData.abilitiesApplied.push(ability.id);
}
if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) {
pokemon.battleData.abilitiesApplied.push(ability.id);
}
if (attr.showAbility && !simulated) {
if (showAbilityInstant) {
globalScene.abilityBar.showAbility(pokemon, passive);
} else {
queueShowAbility(pokemon, passive);
}
}
const message = attr.getTriggerMessage(pokemon, ability.name, args);
if (message) {
if (!simulated) {
globalScene.queueMessage(message);
}
}
messages.push(message!);
}
}
}
class ForceSwitchOutHelper {
constructor(private switchType: SwitchType) {}
@ -5285,6 +5320,21 @@ export function applyPostItemLostAbAttrs(attrType: Constructor<PostItemLostAbAtt
return applyAbAttrsInternal<PostItemLostAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args);
}
/**
* Applies abilities when they become active mid-turn (ability switch)
*
* Ignores passives as they don't change and shouldn't be reapplied when main abilities change
*/
export function applyOnGainAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void {
applySingleAbAttrs<PostSummonAbAttr>(pokemon, passive, PostSummonAbAttr, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, true, simulated);
}
/**
* Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one
*/
export function applyOnLoseClearWeatherAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void {
applySingleAbAttrs<PreLeaveFieldClearWeatherAbAttr>(pokemon, passive, PreLeaveFieldClearWeatherAbAttr, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated);
}
function queueShowAbility(pokemon: Pokemon, passive: boolean): void {
globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive));
globalScene.clearPhaseQueueSplice();

View File

@ -7521,11 +7521,11 @@ export class AbilityChangeAttr extends MoveEffectAttr {
const moveTarget = this.selfTarget ? user : target;
moveTarget.summonData.ability = this.ability;
globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger);
globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name }));
moveTarget.setTempAbility(allAbilities[this.ability]);
globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger);
return true;
}
@ -7548,13 +7548,13 @@ export class AbilityCopyAttr extends MoveEffectAttr {
return false;
}
user.summonData.ability = target.getAbility().id;
globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
user.setTempAbility(target.getAbility());
if (this.copyToPartner && globalScene.currentBattle?.double && user.getAlly().hp) {
user.getAlly().summonData.ability = target.getAbility().id;
globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user.getAlly()), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
user.getAlly().setTempAbility(target.getAbility());
}
return true;
@ -7585,10 +7585,10 @@ export class AbilityGiveAttr extends MoveEffectAttr {
return false;
}
target.summonData.ability = user.getAbility().id;
globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix(target), abilityName: allAbilities[user.getAbility().id].name }));
target.setTempAbility(user.getAbility());
return true;
}
@ -7603,15 +7603,14 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr {
return false;
}
const tempAbilityId = user.getAbility().id;
user.summonData.ability = target.getAbility().id;
target.summonData.ability = tempAbilityId;
const tempAbility = user.getAbility();
globalScene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", { pokemonName: getPokemonNameWithAffix(user) }));
user.setTempAbility(target.getAbility());
target.setTempAbility(tempAbility);
// Swaps Forecast/Flower Gift from Castform/Cherrim
globalScene.arena.triggerWeatherBasedFormChangesToNormal();
// Swaps Forecast/Flower Gift to Castform/Cherrim (edge case)
globalScene.arena.triggerWeatherBasedFormChanges();
return true;
}
@ -7690,7 +7689,6 @@ export class TransformAttr extends MoveEffectAttr {
const promises: Promise<void>[] = [];
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
// Power Trick's effect will not preserved after using Transform
@ -7723,6 +7721,8 @@ export class TransformAttr extends MoveEffectAttr {
promises.push(user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
// If the new ability activates immediately, it needs to happen after all the transform animations
user.setTempAbility(target.getAbility());
}));
await Promise.all(promises);
@ -8035,6 +8035,56 @@ export class AfterYouAttr extends MoveEffectAttr {
}
}
/**
* Move effect to force the target to move last, ignoring priority.
* If applied to multiple targets, they move in speed order after all other moves.
* @extends MoveEffectAttr
*/
export class ForceLastAttr extends MoveEffectAttr {
/**
* Forces the target of this move to move last.
*
* @param user {@linkcode Pokemon} that is using the move.
* @param target {@linkcode Pokemon} that will be forced to move last.
* @param move {@linkcode Move} {@linkcode Moves.QUASH}
* @param _args N/A
* @returns true
*/
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
const targetMovePhase = globalScene.findPhase<MovePhase>((phase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of -
// Either the end of the turn or in front of another, slower move which has also been forced last
const prependPhase = globalScene.findPhase((phase) =>
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|| (phase instanceof MovePhase) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
);
if (prependPhase) {
globalScene.phaseQueue.splice(
globalScene.phaseQueue.indexOf(prependPhase),
0,
new MovePhase(target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true)
);
}
}
return true;
}
}
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !!target.randSeedInt(2);
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}
return phase.isForcedLast() && slower;
};
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
@ -9914,7 +9964,8 @@ export function initMoves() {
.attr(RemoveHeldItemAttr, true),
new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5)
.condition(failIfSingleBattle)
.unimplemented(),
.condition((user, target, move) => !target.turnData.acted)
.attr(ForceLastAttr),
new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))),
new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5)

View File

@ -64,7 +64,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
import { WeatherType } from "#enums/weather-type";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import type { Ability, AbAttr } from "#app/data/ability";
import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs } from "#app/data/ability";
import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnLoseClearWeatherAbAttrs } from "#app/data/ability";
import type PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui";
@ -1481,6 +1481,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return abilityAttrs;
}
/**
* Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon
*
* Also clears primal weather if it is from the ability being changed
* @param ability New Ability
*/
public setTempAbility(ability: Ability, passive: boolean = false): void {
applyOnLoseClearWeatherAbAttrs(this, passive);
if (passive) {
this.summonData.passiveAbility = ability.id;
} else {
this.summonData.ability = ability.id;
}
applyOnGainAbAttrs(this, passive);
}
/**
* Checks if a pokemon has a passive either from:
* - bought with starter candy

View File

@ -1822,7 +1822,7 @@ const modifierPool: ModifierPool = {
if (!isHoldingOrb) {
const moveset = p.getMoveset(true).filter(m => !isNullOrUndefined(m)).map(m => m.moveId);
const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true);
const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true);
// Moves that take advantage of obtaining the actual status effect
const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ]

View File

@ -56,6 +56,7 @@ export class MovePhase extends BattlePhase {
protected _targets: BattlerIndex[];
protected followUp: boolean;
protected ignorePp: boolean;
protected forcedLast: boolean;
protected failed: boolean = false;
protected cancelled: boolean = false;
protected reflected: boolean = false;
@ -90,7 +91,8 @@ export class MovePhase extends BattlePhase {
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce.
* Reflected moves cannot be reflected again and will not trigger Dancer.
*/
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) {
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false, forcedLast: boolean = false) {
super();
this.pokemon = pokemon;
@ -99,6 +101,7 @@ export class MovePhase extends BattlePhase {
this.followUp = followUp;
this.ignorePp = ignorePp;
this.reflected = reflected;
this.forcedLast = forcedLast;
}
/**
@ -120,6 +123,15 @@ export class MovePhase extends BattlePhase {
this.cancelled = true;
}
/**
* Shows whether the current move has been forced to the end of the turn
* Needed for speed order, see {@linkcode Moves.QUASH}
* */
public isForcedLast(): boolean {
return this.forcedLast;
}
public start(): void {
super.start();

View File

@ -116,4 +116,15 @@ describe("Abilities - Imposter", () => {
}
});
});
it("should activate its ability if it copies one that activates on summon", async () => {
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.DITTO ]);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("MoveEndPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});

View File

@ -0,0 +1,52 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Mummy", () => {
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.MUMMY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
});
it("should set the enemy's ability to mummy when hit by a contact move", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.MUMMY);
});
it("should not change the enemy's ability hit by a non-contact move", async () => {
game.override.enemyMoveset(Moves.EARTHQUAKE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
});
});

View File

@ -0,0 +1,53 @@
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Trace", () => {
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.TRACE)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should copy the opponent's ability", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
});
it("should activate a copied post-summon ability", async () => {
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});

View File

@ -0,0 +1,65 @@
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Wandering Spirit", () => {
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.WANDERING_SPIRIT)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
});
it("should exchange abilities when hit with a contact move", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.WANDERING_SPIRIT);
});
it("should not exchange abilities when hit with a non-contact move", async () => {
game.override.enemyMoveset(Moves.EARTHQUAKE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.WANDERING_SPIRIT);
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
});
it("should activate post-summon abilities", async () => {
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});

View File

@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import GameManager from "#app/test/utils/gameManager";
import { toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
@ -244,29 +245,32 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch();
});
it("If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery", async () => {
game.override
.moveset([ Moves.DOUBLE_EDGE ])
.enemyMoveset([ Moves.SPLASH ])
.startingHeldItems([
{ name: "SHELL_BELL", count: 3 },
{ name: "HEALING_CHARM", count: 5 },
]);
await game.classicMode.startBattle([
Species.WIMPOD,
Species.TYRUNT
]);
// TODO: Enable when this behavior is fixed (currently Shell Bell won't activate if Wimp Out activates because
// the pokemon is removed from the field before the Shell Bell modifier is applied, so it can't see the
// damage dealt and doesn't heal the pokemon)
it.todo(
"If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery",
async () => {
game.override
.moveset([ Moves.DOUBLE_EDGE ])
.enemyMoveset([ Moves.SPLASH ])
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]);
await game.classicMode.startBattle([ Species.WIMPOD, Species.TYRUNT ]);
game.scene.getPlayerPokemon()!.hp *= 0.75;
const wimpod = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
wimpod.damageAndUpdate(toDmgValue(wimpod.getMaxHp() * 0.4));
expect(game.scene.getPlayerParty()[1].getHpRatio()).toBeGreaterThan(0.5);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
});
game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[1]).toBe(wimpod);
expect(wimpod.hp).toBeGreaterThan(toDmgValue(wimpod.getMaxHp() / 2));
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
},
);
it("Wimp Out will activate due to weather damage", async () => {
game.override

View File

@ -0,0 +1,67 @@
import { allAbilities } from "#app/data/ability";
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Test Ability Swapping", () => {
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.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should activate post-summon abilities", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.INTIMIDATE]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
it("should remove primal weather when the setter's ability is removed", async () => {
game.override.ability(Abilities.DESOLATE_LAND);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.BALL_FETCH]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.weather?.weatherType).toBeUndefined();
});
it("should not activate passive abilities", async () => {
game.override.passiveAbility(Abilities.INTREPID_SWORD);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SPLASH);
game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.BALL_FETCH]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1); // would be 2 if passive activated again
});
});

View File

@ -0,0 +1,70 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Doodle", () => {
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, Moves.DOODLE ])
.ability(Abilities.ADAPTABILITY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should copy the opponent's ability in singles", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.DOODLE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
});
it("should copy the opponent's ability to itself and its ally in doubles", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.DOODLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerField()[0].getAbility().id).toBe(Abilities.BALL_FETCH);
expect(game.scene.getPlayerField()[1].getAbility().id).toBe(Abilities.BALL_FETCH);
});
it("should activate post-summon abilities", async () => {
game.override.battleType("double")
.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.DOODLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
// Enemies should have been intimidated twice
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-2);
});
});

View File

@ -0,0 +1,53 @@
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Entrainment", () => {
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, Moves.ENTRAINMENT ])
.ability(Abilities.ADAPTABILITY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("gives its ability to the target", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.ENTRAINMENT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.ADAPTABILITY);
});
it("should activate post-summon abilities", async () => {
game.override.ability(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.ENTRAINMENT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});

View File

@ -0,0 +1,99 @@
import { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { Abilities } from "#app/enums/abilities";
import { BattlerIndex } from "#app/battle";
import { WeatherType } from "#enums/weather-type";
import { MoveResult } from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest";
describe("Moves - Quash", () => {
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
.battleType("double")
.enemyLevel(1)
.enemySpecies(Species.SLOWPOKE)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.moveset([ Moves.QUASH, Moves.SUNNY_DAY, Moves.RAIN_DANCE, Moves.SPLASH ]);
});
it("makes the target move last in a turn, ignoring priority", async () => {
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.QUASH, 0, BattlerIndex.PLAYER_2);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.RAIN_DANCE);
await game.phaseInterceptor.to("TurnEndPhase", false);
// will be sunny if player_2 moved last because of quash, rainy otherwise
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("fails if the target has already moved", async () => {
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.QUASH, 1, BattlerIndex.PLAYER);
await game.phaseInterceptor.to("MoveEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
});
it("makes multiple quashed targets move in speed order at the end of the turn", async () => {
game.override.enemySpecies(Species.NINJASK)
.enemyLevel(100);
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
// both users are quashed - rattata is slower so sun should be up at end of turn
game.move.select(Moves.RAIN_DANCE, 0);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("respects trick room", async () => {
game.override.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH, Moves.TRICK_ROOM ]);
await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TRICK_ROOM);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnInitPhase");
// both users are quashed - accelgor should move last w/ TR so rain should be up at end of turn
game.move.select(Moves.RAIN_DANCE, 0);
game.move.select(Moves.SUNNY_DAY, 1);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("TurnEndPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN);
});
});

View File

@ -0,0 +1,53 @@
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Role Play", () => {
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, Moves.ROLE_PLAY ])
.ability(Abilities.ADAPTABILITY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should set the user's ability to the target's ability", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.ROLE_PLAY);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
});
it("should activate post-summon abilities", async () => {
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.ROLE_PLAY);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});

View File

@ -0,0 +1,42 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Simple Beam", () => {
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, Moves.SIMPLE_BEAM ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("sets the target's ability to simple", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SIMPLE_BEAM);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.SIMPLE);
});
});

View File

@ -0,0 +1,56 @@
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Skill Swap", () => {
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, Moves.SKILL_SWAP ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should swap the two abilities", async () => {
game.override.ability(Abilities.ADAPTABILITY);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SKILL_SWAP);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH);
expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.ADAPTABILITY);
});
it("should activate post-summon abilities", async () => {
game.override.ability(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SKILL_SWAP);
await game.phaseInterceptor.to("BerryPhase");
// player atk should be -1 after opponent gains intimidate and it activates
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});

View File

@ -116,4 +116,16 @@ describe("Moves - Transform", () => {
}
});
});
it("should activate its ability if it copies one that activates on summon", async () => {
game.override.enemyAbility(Abilities.INTIMIDATE)
.ability(Abilities.BALL_FETCH);
await game.classicMode.startBattle([ Species.DITTO ]);
game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
});