mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Compare commits
10 Commits
4aae2f8b27
...
1f638a2618
Author | SHA1 | Date | |
---|---|---|---|
|
1f638a2618 | ||
|
46c78a0540 | ||
|
98809c28bd | ||
|
e0559e03ff | ||
|
f6b99780fb | ||
|
19af9bdb8b | ||
|
8e61b642a3 | ||
|
a29161c2ed | ||
|
314f46a22b | ||
|
4885a6abc5 |
@ -1 +1 @@
|
||||
Subproject commit ab2716d5440c25f73986664aa3f3131821c3c392
|
||||
Subproject commit 1ea8f865e30d1940caa0fceeabf37ae2e4689471
|
@ -1152,13 +1152,16 @@ export class GravityTag extends SerializableArenaTag {
|
||||
|
||||
onAdd(_arena: Arena): void {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
|
||||
|
||||
// Remove all flying-related effects from all on-field Pokemon.
|
||||
globalScene.getField(true).forEach(pokemon => {
|
||||
if (pokemon !== null) {
|
||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||
pokemon.removeTag(BattlerTagType.FLYING);
|
||||
// TODO: This is an extremely poor way of handling move interruption
|
||||
|
||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -45736,6 +45736,285 @@ export const tmSpecies: TmSpecies = {
|
||||
SpeciesId.HISUI_ARCANINE,
|
||||
SpeciesId.HISUI_AVALUGG,
|
||||
],
|
||||
[MoveId.SHOCK_WAVE]: [
|
||||
SpeciesId.RATTATA,
|
||||
SpeciesId.RATICATE,
|
||||
SpeciesId.PIKACHU,
|
||||
SpeciesId.RAICHU,
|
||||
SpeciesId.NIDORAN_F,
|
||||
SpeciesId.NIDORINA,
|
||||
SpeciesId.NIDOQUEEN,
|
||||
SpeciesId.NIDORAN_M,
|
||||
SpeciesId.NIDORINO,
|
||||
SpeciesId.NIDOKING,
|
||||
SpeciesId.CLEFAIRY,
|
||||
SpeciesId.CLEFABLE,
|
||||
SpeciesId.JIGGLYPUFF,
|
||||
SpeciesId.WIGGLYTUFF,
|
||||
SpeciesId.MEOWTH,
|
||||
SpeciesId.PERSIAN,
|
||||
SpeciesId.ABRA,
|
||||
SpeciesId.KADABRA,
|
||||
SpeciesId.ALAKAZAM,
|
||||
SpeciesId.MAGNEMITE,
|
||||
SpeciesId.MAGNETON,
|
||||
SpeciesId.GRIMER,
|
||||
SpeciesId.MUK,
|
||||
SpeciesId.VOLTORB,
|
||||
SpeciesId.ELECTRODE,
|
||||
SpeciesId.LICKITUNG,
|
||||
SpeciesId.KOFFING,
|
||||
SpeciesId.WEEZING,
|
||||
SpeciesId.RHYHORN,
|
||||
SpeciesId.RHYDON,
|
||||
SpeciesId.CHANSEY,
|
||||
SpeciesId.TANGELA,
|
||||
SpeciesId.KANGASKHAN,
|
||||
SpeciesId.MR_MIME,
|
||||
SpeciesId.ELECTABUZZ,
|
||||
SpeciesId.TAUROS,
|
||||
SpeciesId.LAPRAS,
|
||||
SpeciesId.JOLTEON,
|
||||
SpeciesId.PORYGON,
|
||||
SpeciesId.SNORLAX,
|
||||
SpeciesId.ZAPDOS,
|
||||
SpeciesId.DRATINI,
|
||||
SpeciesId.DRAGONAIR,
|
||||
SpeciesId.DRAGONITE,
|
||||
SpeciesId.MEWTWO,
|
||||
SpeciesId.MEW,
|
||||
SpeciesId.SENTRET,
|
||||
SpeciesId.FURRET,
|
||||
SpeciesId.CHINCHOU,
|
||||
SpeciesId.LANTURN,
|
||||
SpeciesId.PICHU,
|
||||
SpeciesId.CLEFFA,
|
||||
SpeciesId.IGGLYBUFF,
|
||||
SpeciesId.TOGEPI,
|
||||
SpeciesId.TOGETIC,
|
||||
SpeciesId.MAREEP,
|
||||
SpeciesId.FLAAFFY,
|
||||
SpeciesId.AMPHAROS,
|
||||
SpeciesId.AIPOM,
|
||||
SpeciesId.MISDREAVUS,
|
||||
SpeciesId.GIRAFARIG,
|
||||
SpeciesId.DUNSPARCE,
|
||||
SpeciesId.SNUBBULL,
|
||||
SpeciesId.GRANBULL,
|
||||
SpeciesId.QWILFISH,
|
||||
SpeciesId.PORYGON2,
|
||||
SpeciesId.STANTLER,
|
||||
SpeciesId.ELEKID,
|
||||
SpeciesId.MILTANK,
|
||||
SpeciesId.BLISSEY,
|
||||
SpeciesId.RAIKOU,
|
||||
SpeciesId.TYRANITAR,
|
||||
SpeciesId.LUGIA,
|
||||
SpeciesId.HO_OH,
|
||||
SpeciesId.CELEBI,
|
||||
SpeciesId.ZIGZAGOON,
|
||||
SpeciesId.LINOONE,
|
||||
SpeciesId.WINGULL,
|
||||
SpeciesId.PELIPPER,
|
||||
SpeciesId.RALTS,
|
||||
SpeciesId.KIRLIA,
|
||||
SpeciesId.GARDEVOIR,
|
||||
SpeciesId.SLAKOTH,
|
||||
SpeciesId.VIGOROTH,
|
||||
SpeciesId.SLAKING,
|
||||
SpeciesId.WHISMUR,
|
||||
SpeciesId.LOUDRED,
|
||||
SpeciesId.EXPLOUD,
|
||||
SpeciesId.NOSEPASS,
|
||||
SpeciesId.SKITTY,
|
||||
SpeciesId.DELCATTY,
|
||||
SpeciesId.SABLEYE,
|
||||
SpeciesId.ARON,
|
||||
SpeciesId.LAIRON,
|
||||
SpeciesId.AGGRON,
|
||||
SpeciesId.ELECTRIKE,
|
||||
SpeciesId.MANECTRIC,
|
||||
SpeciesId.PLUSLE,
|
||||
SpeciesId.MINUN,
|
||||
SpeciesId.VOLBEAT,
|
||||
SpeciesId.ILLUMISE,
|
||||
SpeciesId.GULPIN,
|
||||
SpeciesId.SWALOT,
|
||||
SpeciesId.SPOINK,
|
||||
SpeciesId.GRUMPIG,
|
||||
SpeciesId.SPINDA,
|
||||
SpeciesId.ZANGOOSE,
|
||||
SpeciesId.CASTFORM,
|
||||
SpeciesId.KECLEON,
|
||||
SpeciesId.SHUPPET,
|
||||
SpeciesId.BANETTE,
|
||||
SpeciesId.CHIMECHO,
|
||||
SpeciesId.ABSOL,
|
||||
SpeciesId.REGIROCK,
|
||||
SpeciesId.REGICE,
|
||||
SpeciesId.REGISTEEL,
|
||||
SpeciesId.LATIAS,
|
||||
SpeciesId.LATIOS,
|
||||
SpeciesId.KYOGRE,
|
||||
SpeciesId.GROUDON,
|
||||
SpeciesId.RAYQUAZA,
|
||||
SpeciesId.JIRACHI,
|
||||
SpeciesId.DEOXYS,
|
||||
SpeciesId.BIDOOF,
|
||||
SpeciesId.BIBAREL,
|
||||
SpeciesId.SHINX,
|
||||
SpeciesId.LUXIO,
|
||||
SpeciesId.LUXRAY,
|
||||
SpeciesId.CRANIDOS,
|
||||
SpeciesId.RAMPARDOS,
|
||||
SpeciesId.SHIELDON,
|
||||
SpeciesId.BASTIODON,
|
||||
SpeciesId.PACHIRISU,
|
||||
SpeciesId.AMBIPOM,
|
||||
SpeciesId.DRIFLOON,
|
||||
SpeciesId.DRIFBLIM,
|
||||
SpeciesId.BUNEARY,
|
||||
SpeciesId.LOPUNNY,
|
||||
SpeciesId.MISMAGIUS,
|
||||
SpeciesId.GLAMEOW,
|
||||
SpeciesId.PURUGLY,
|
||||
SpeciesId.CHINGLING,
|
||||
SpeciesId.MIME_JR,
|
||||
SpeciesId.HAPPINY,
|
||||
SpeciesId.SPIRITOMB,
|
||||
SpeciesId.MUNCHLAX,
|
||||
SpeciesId.MAGNEZONE,
|
||||
SpeciesId.LICKILICKY,
|
||||
SpeciesId.RHYPERIOR,
|
||||
SpeciesId.TANGROWTH,
|
||||
SpeciesId.ELECTIVIRE,
|
||||
SpeciesId.TOGEKISS,
|
||||
SpeciesId.PORYGON_Z,
|
||||
SpeciesId.GALLADE,
|
||||
SpeciesId.PROBOPASS,
|
||||
SpeciesId.FROSLASS,
|
||||
SpeciesId.ROTOM,
|
||||
SpeciesId.UXIE,
|
||||
SpeciesId.MESPRIT,
|
||||
SpeciesId.AZELF,
|
||||
SpeciesId.DIALGA,
|
||||
SpeciesId.PALKIA,
|
||||
SpeciesId.REGIGIGAS,
|
||||
SpeciesId.GIRATINA,
|
||||
SpeciesId.DARKRAI,
|
||||
SpeciesId.ARCEUS,
|
||||
SpeciesId.VICTINI,
|
||||
SpeciesId.PATRAT,
|
||||
SpeciesId.WATCHOG,
|
||||
SpeciesId.LILLIPUP,
|
||||
SpeciesId.HERDIER,
|
||||
SpeciesId.STOUTLAND,
|
||||
SpeciesId.MUNNA,
|
||||
SpeciesId.MUSHARNA,
|
||||
SpeciesId.BLITZLE,
|
||||
SpeciesId.ZEBSTRIKA,
|
||||
SpeciesId.WOOBAT,
|
||||
SpeciesId.SWOOBAT,
|
||||
SpeciesId.SIGILYPH,
|
||||
SpeciesId.YAMASK,
|
||||
SpeciesId.COFAGRIGUS,
|
||||
SpeciesId.MINCCINO,
|
||||
SpeciesId.CINCCINO,
|
||||
SpeciesId.GOTHITA,
|
||||
SpeciesId.GOTHORITA,
|
||||
SpeciesId.GOTHITELLE,
|
||||
SpeciesId.SOLOSIS,
|
||||
SpeciesId.DUOSION,
|
||||
SpeciesId.REUNICLUS,
|
||||
SpeciesId.EMOLGA,
|
||||
SpeciesId.FRILLISH,
|
||||
SpeciesId.JELLICENT,
|
||||
SpeciesId.JOLTIK,
|
||||
SpeciesId.GALVANTULA,
|
||||
SpeciesId.KLINK,
|
||||
SpeciesId.KLANG,
|
||||
SpeciesId.KLINKLANG,
|
||||
SpeciesId.EELEKTRIK,
|
||||
SpeciesId.EELEKTROSS,
|
||||
SpeciesId.ELGYEM,
|
||||
SpeciesId.BEHEEYEM,
|
||||
SpeciesId.LITWICK,
|
||||
SpeciesId.LAMPENT,
|
||||
SpeciesId.CHANDELURE,
|
||||
SpeciesId.AXEW,
|
||||
SpeciesId.FRAXURE,
|
||||
SpeciesId.HAXORUS,
|
||||
SpeciesId.STUNFISK,
|
||||
SpeciesId.DRUDDIGON,
|
||||
SpeciesId.GOLETT,
|
||||
SpeciesId.GOLURK,
|
||||
SpeciesId.DEINO,
|
||||
SpeciesId.ZWEILOUS,
|
||||
SpeciesId.HYDREIGON,
|
||||
SpeciesId.THUNDURUS,
|
||||
SpeciesId.ZEKROM,
|
||||
SpeciesId.MELOETTA,
|
||||
SpeciesId.GENESECT,
|
||||
SpeciesId.BRAIXEN,
|
||||
SpeciesId.DELPHOX,
|
||||
SpeciesId.ESPURR,
|
||||
SpeciesId.MEOWSTIC,
|
||||
SpeciesId.HONEDGE,
|
||||
SpeciesId.DOUBLADE,
|
||||
SpeciesId.AEGISLASH,
|
||||
SpeciesId.SKRELP,
|
||||
SpeciesId.DRAGALGE,
|
||||
SpeciesId.HELIOPTILE,
|
||||
SpeciesId.HELIOLISK,
|
||||
SpeciesId.DEDENNE,
|
||||
SpeciesId.GOOMY,
|
||||
SpeciesId.SLIGGOO,
|
||||
SpeciesId.GOODRA,
|
||||
SpeciesId.ZYGARDE,
|
||||
SpeciesId.HOOPA,
|
||||
SpeciesId.YUNGOOS,
|
||||
SpeciesId.GUMSHOOS,
|
||||
SpeciesId.GRUBBIN,
|
||||
SpeciesId.CHARJABUG,
|
||||
SpeciesId.VIKAVOLT,
|
||||
SpeciesId.PASSIMIAN,
|
||||
SpeciesId.TURTONATOR,
|
||||
SpeciesId.TOGEDEMARU,
|
||||
SpeciesId.DRAMPA,
|
||||
SpeciesId.KOMMO_O,
|
||||
SpeciesId.TAPU_KOKO,
|
||||
SpeciesId.SOLGALEO,
|
||||
SpeciesId.LUNALA,
|
||||
SpeciesId.PHEROMOSA,
|
||||
SpeciesId.XURKITREE,
|
||||
SpeciesId.CELESTEELA,
|
||||
SpeciesId.GUZZLORD,
|
||||
SpeciesId.NECROZMA,
|
||||
SpeciesId.MAGEARNA,
|
||||
SpeciesId.NAGANADEL,
|
||||
SpeciesId.ZERAORA,
|
||||
SpeciesId.TOXTRICITY,
|
||||
SpeciesId.MR_RIME,
|
||||
SpeciesId.REGIELEKI,
|
||||
SpeciesId.WYRDEER,
|
||||
SpeciesId.FARIGIRAF,
|
||||
SpeciesId.DUDUNSPARCE,
|
||||
SpeciesId.MIRAIDON,
|
||||
SpeciesId.RAGING_BOLT,
|
||||
SpeciesId.ALOLA_RATTATA,
|
||||
SpeciesId.ALOLA_RATICATE,
|
||||
SpeciesId.ALOLA_RAICHU,
|
||||
SpeciesId.ALOLA_MEOWTH,
|
||||
SpeciesId.ALOLA_PERSIAN,
|
||||
SpeciesId.ALOLA_GRAVELER,
|
||||
SpeciesId.ALOLA_GOLEM,
|
||||
SpeciesId.ALOLA_GRIMER,
|
||||
SpeciesId.ALOLA_MUK,
|
||||
SpeciesId.GALAR_WEEZING,
|
||||
SpeciesId.GALAR_MR_MIME,
|
||||
SpeciesId.HISUI_SLIGGOO,
|
||||
SpeciesId.HISUI_GOODRA,
|
||||
],
|
||||
[MoveId.WATER_PULSE]: [
|
||||
SpeciesId.SQUIRTLE,
|
||||
SpeciesId.WARTORTLE,
|
||||
@ -68747,6 +69026,7 @@ export const tmPoolTiers: TmPoolTiers = {
|
||||
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA,
|
||||
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT,
|
||||
[MoveId.ROCK_BLAST]: ModifierTier.GREAT,
|
||||
[MoveId.SHOCK_WAVE]: ModifierTier.GREAT,
|
||||
[MoveId.WATER_PULSE]: ModifierTier.GREAT,
|
||||
[MoveId.ROOST]: ModifierTier.GREAT,
|
||||
[MoveId.GRAVITY]: ModifierTier.COMMON,
|
||||
|
@ -736,6 +736,10 @@ export class FlinchedTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag to cancel the target's action when knocked out of a flying move by Smack Down or Gravity.
|
||||
*/
|
||||
// TODO: This is not a very good way to cancel a semi invulnerable turn
|
||||
export class InterruptedTag extends BattlerTag {
|
||||
public override readonly tagType = BattlerTagType.INTERRUPTED;
|
||||
constructor(sourceMove: MoveId) {
|
||||
@ -765,7 +769,7 @@ export class InterruptedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition) Confusion} status condition
|
||||
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition) | Confusion} status condition
|
||||
*/
|
||||
export class ConfusedTag extends SerializableBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.CONFUSED;
|
||||
@ -774,8 +778,9 @@ export class ConfusedTag extends SerializableBattlerTag {
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.MISTY;
|
||||
const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.MISTY;
|
||||
if (blockedByTerrain) {
|
||||
// TODO: this should not trigger if the current move is an attacking move
|
||||
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
|
||||
return false;
|
||||
}
|
||||
|
@ -1536,6 +1536,7 @@ export class CritOnlyAttr extends MoveAttr {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix subclasses to actually extend from `getDamage`
|
||||
export class FixedDamageAttr extends MoveAttr {
|
||||
private damage: number;
|
||||
|
||||
@ -5372,13 +5373,11 @@ export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
||||
if (target.isOfType(PokemonType.FLYING)) {
|
||||
multiplier.value = 1;
|
||||
}
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||
if (!target.isGrounded(true) && target.isOfType(PokemonType.FLYING)) {
|
||||
const multiplier = args[0];
|
||||
// When a flying type is hit, the first hit is always 1x multiplier.
|
||||
multiplier.value = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -5617,13 +5616,13 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
protected cancelOnFail: boolean;
|
||||
private failOnOverlap: boolean;
|
||||
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) {
|
||||
constructor(tagType: BattlerTagType, selfTarget = false, failOnOverlap = false, turnCountMin: number = 0, turnCountMax = turnCountMin, lastHitOnly = false) {
|
||||
super(selfTarget, { lastHitOnly: lastHitOnly });
|
||||
|
||||
this.tagType = tagType;
|
||||
this.turnCountMin = turnCountMin;
|
||||
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
|
||||
this.failOnOverlap = !!failOnOverlap;
|
||||
this.turnCountMax = turnCountMax;
|
||||
this.failOnOverlap = failOnOverlap;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -5633,13 +5632,14 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
if (moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) {
|
||||
return (this.selfTarget ? user : target).addTag(this.tagType, user.randBattleSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id);
|
||||
return (this.selfTarget ? user : target).addTag(this.tagType, user.randBattleSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc | null {
|
||||
// TODO: This should consider whether the tag can be added
|
||||
return this.failOnOverlap
|
||||
? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType)
|
||||
: null;
|
||||
@ -5717,8 +5717,10 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the appropriate battler tag for Smack Down and Thousand arrows
|
||||
* @extends AddBattlerTagAttr
|
||||
* Attribute to add the {@linkcode BattlerTagType.IGNORE_FLYING | IGNORE_FLYING} battler tag to the target
|
||||
* and remove any prior sources of ungroundedness.
|
||||
*
|
||||
* Does nothing if the target was not already ungrounded.
|
||||
*/
|
||||
export class FallDownAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
@ -5726,18 +5728,35 @@ export class FallDownAttr extends AddBattlerTagAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Grounded Tag to the target and checks if fallDown message should be displayed
|
||||
* @param user the {@linkcode Pokemon} using the move
|
||||
* @param target the {@linkcode Pokemon} targeted by the move
|
||||
* @param move the {@linkcode Move} invoking this effect
|
||||
* Add `GroundedTag` to the target, remove all prior sources of ungroundedness
|
||||
* and display a message.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param move - The {@linkcode Move} invoking this effect
|
||||
* @param args n/a
|
||||
* @returns `true` if the effect successfully applies; `false` otherwise
|
||||
* @returns Whether the target was successfully brought down to earth.
|
||||
*
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.isGrounded()) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
||||
// Smack down and similar only add their tag if the target is already ungrounded,
|
||||
// barring any prior semi-invulnerability.
|
||||
if (target.isGrounded(true)) {
|
||||
return false;
|
||||
}
|
||||
return super.apply(user, target, move, args);
|
||||
|
||||
// Remove the target's prior sources of ungroundedness.
|
||||
// NB: These effects cannot simply be part of the tag's `onAdd` effect as Ingrain also adds the tag
|
||||
// but does not remove Telekinesis' accuracy boost
|
||||
target.removeTag(BattlerTagType.FLOATING);
|
||||
target.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (target.getTag(BattlerTagType.FLYING)) {
|
||||
target.removeTag(BattlerTagType.FLYING);
|
||||
// TODO: This is an extremely poor way of handling move interruption
|
||||
target.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
return super.apply(user, target, move, _args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5841,6 +5860,7 @@ export class CurseAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Delete this and make mortal spin use `RemoveBattlerTagAttr`
|
||||
export class LapseBattlerTagAttr extends MoveEffectAttr {
|
||||
public tagTypes: BattlerTagType[];
|
||||
|
||||
@ -8040,7 +8060,12 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
|
||||
return phase.isForcedLast() && slower;
|
||||
};
|
||||
|
||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||
// #region Condition functions
|
||||
|
||||
// TODO: This needs to become unselectable, not merely fail
|
||||
const failOnGravityCondition: MoveConditionFunc = () => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||
|
||||
const failOnGroundedCondition: MoveConditionFunc = (_user, target) => !target.getTag(BattlerTagType.IGNORE_FLYING);
|
||||
|
||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||
|
||||
@ -8071,6 +8096,10 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke
|
||||
|
||||
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
||||
|
||||
const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(PokemonType.UNKNOWN);
|
||||
|
||||
// #endregion Condition functions
|
||||
|
||||
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||
if (heldItems.length === 0) {
|
||||
@ -8252,9 +8281,6 @@ export class ExposedMoveAttr extends AddBattlerTagAttr {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(PokemonType.UNKNOWN);
|
||||
|
||||
export type MoveTargetSet = {
|
||||
targets: BattlerIndex[];
|
||||
multiple: boolean;
|
||||
@ -8901,7 +8927,6 @@ export function initMoves() {
|
||||
.ignoresProtect()
|
||||
/* Transform:
|
||||
* Does not copy the target's rage fist hit count
|
||||
* Does not copy the target's volatile status conditions (ie BattlerTags)
|
||||
* Renders user typeless when copying typeless opponent (should revert to original typing)
|
||||
*/
|
||||
.edgeCase(),
|
||||
@ -9356,6 +9381,7 @@ export function initMoves() {
|
||||
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
||||
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true)
|
||||
// NB: We add IGNORE_FLYING and remove floating tag directly to avoid removing Telekinesis' accuracy boost
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true),
|
||||
new AttackMove(MoveId.SUPERPOWER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
||||
@ -9613,9 +9639,9 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false)
|
||||
.triageMove(),
|
||||
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
||||
.ignoresProtect()
|
||||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
.target(MoveTarget.BOTH_SIDES)
|
||||
.ignoresProtect(),
|
||||
new StatusMove(MoveId.MIRACLE_EYE, PokemonType.PSYCHIC, -1, 40, -1, 0, 4)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
||||
.ignoresSubstitute()
|
||||
@ -9745,7 +9771,8 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
||||
.condition((user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))),
|
||||
.condition(failOnGravityCondition)
|
||||
.condition(failOnGroundedCondition),
|
||||
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
||||
.attr(RecoilAttr, false, 0.33)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||
@ -9974,12 +10001,12 @@ export function initMoves() {
|
||||
.powderMove()
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
|
||||
.condition(failOnGravityCondition)
|
||||
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
||||
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
||||
.condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
|
||||
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
||||
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
||||
.condition(failOnGravityCondition)
|
||||
.condition(failOnGroundedCondition)
|
||||
.reflectable(),
|
||||
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
||||
.ignoresProtect()
|
||||
@ -9987,8 +10014,6 @@ export function initMoves() {
|
||||
.unimplemented(),
|
||||
new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
|
||||
.attr(FallDownAttr)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||
.makesContact(false),
|
||||
new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||
@ -10450,8 +10475,6 @@ export function initMoves() {
|
||||
.attr(FallDownAttr)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6)
|
||||
|
@ -99,6 +99,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
|
||||
MysteryEncounterType.DARK_DEAL,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.ROGUE)
|
||||
.withDisallowedChallenges(Challenges.HARDCORE)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "dark_deal_scientist",
|
||||
|
@ -673,6 +673,8 @@ export async function catchPokemon(
|
||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
const doPokemonCatchMenu = () => {
|
||||
const end = () => {
|
||||
// Ensure the pokemon is in the enemy party in all situations
|
||||
@ -708,9 +710,7 @@ export async function catchPokemon(
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
if (!addStatus.value) {
|
||||
if (!(isObtain || addStatus.value)) {
|
||||
removePokemon();
|
||||
end();
|
||||
return;
|
||||
@ -807,10 +807,16 @@ export async function catchPokemon(
|
||||
};
|
||||
|
||||
if (showCatchObtainMessage) {
|
||||
let catchMessage: string;
|
||||
if (isObtain) {
|
||||
catchMessage = "battle:pokemonObtained";
|
||||
} else if (addStatus.value) {
|
||||
catchMessage = "battle:pokemonCaught";
|
||||
} else {
|
||||
catchMessage = "battle:pokemonCaughtButChallenge";
|
||||
}
|
||||
globalScene.ui.showText(
|
||||
i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
i18next.t(catchMessage, { pokemonName: pokemon.getNameToRender() }),
|
||||
null,
|
||||
doPokemonCatchMenu,
|
||||
0,
|
||||
|
@ -23,17 +23,21 @@ export class Terrain {
|
||||
public terrainType: TerrainType;
|
||||
public turnsLeft: number;
|
||||
|
||||
constructor(terrainType: TerrainType, turnsLeft?: number) {
|
||||
constructor(terrainType: TerrainType, turnsLeft = 0) {
|
||||
this.terrainType = terrainType;
|
||||
this.turnsLeft = turnsLeft || 0;
|
||||
this.turnsLeft = turnsLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick down this terrain's duration.
|
||||
* @returns Whether the current terrain should remain active (`turnsLeft > 0`)
|
||||
*/
|
||||
lapse(): boolean {
|
||||
if (this.turnsLeft) {
|
||||
return !!--this.turnsLeft;
|
||||
// TODO: Add separate flag for infinite duration terrains
|
||||
if (this.turnsLeft <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return --this.turnsLeft > 0;
|
||||
}
|
||||
|
||||
getAttackTypeMultiplier(attackType: PokemonType): number {
|
||||
|
@ -20,20 +20,25 @@ export class Weather {
|
||||
public weatherType: WeatherType;
|
||||
public turnsLeft: number;
|
||||
|
||||
constructor(weatherType: WeatherType, turnsLeft?: number) {
|
||||
constructor(weatherType: WeatherType, turnsLeft = 0) {
|
||||
this.weatherType = weatherType;
|
||||
this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0;
|
||||
this.turnsLeft = this.isImmutable() ? 0 : turnsLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick down this weather's duration.
|
||||
* @returns Whether the current weather should remain active (`turnsLeft > 0`)
|
||||
*/
|
||||
lapse(): boolean {
|
||||
if (this.isImmutable()) {
|
||||
return true;
|
||||
}
|
||||
if (this.turnsLeft) {
|
||||
return !!--this.turnsLeft;
|
||||
|
||||
if (this.turnsLeft <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return --this.turnsLeft > 0;
|
||||
}
|
||||
|
||||
isImmutable(): boolean {
|
||||
@ -127,6 +132,7 @@ export class Weather {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: These functions should return empty strings instead of `null` - requires bangs
|
||||
export function getWeatherStartMessage(weatherType: WeatherType): string | null {
|
||||
switch (weatherType) {
|
||||
case WeatherType.SUNNY:
|
||||
|
@ -288,20 +288,18 @@ export class Arena {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets weather to the override specified in overrides.ts
|
||||
* @param weather new {@linkcode WeatherType} to set
|
||||
* @returns true to force trySetWeather to return true
|
||||
* Sets weather to the override specified in overrides.ts`
|
||||
*/
|
||||
trySetWeatherOverride(weather: WeatherType): boolean {
|
||||
private overrideWeather(): void {
|
||||
const weather = Overrides.WEATHER_OVERRIDE;
|
||||
this.weather = new Weather(weather, 0);
|
||||
globalScene.phaseManager.unshiftNew("CommonAnimPhase", undefined, undefined, CommonAnim.SUNNY + (weather - 1));
|
||||
globalScene.phaseManager.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct?
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns weather or not the weather can be changed to {@linkcode weather} */
|
||||
canSetWeather(weather: WeatherType): boolean {
|
||||
return !(this.weather?.weatherType === (weather || undefined));
|
||||
return this.getWeatherType() !== weather;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -312,14 +310,15 @@ export class Arena {
|
||||
*/
|
||||
trySetWeather(weather: WeatherType, user?: Pokemon): boolean {
|
||||
if (Overrides.WEATHER_OVERRIDE) {
|
||||
return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE);
|
||||
this.overrideWeather();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.canSetWeather(weather)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldWeatherType = this.weather?.weatherType || WeatherType.NONE;
|
||||
const oldWeatherType = this.getWeatherType();
|
||||
|
||||
if (
|
||||
this.weather?.isImmutable() &&
|
||||
@ -343,7 +342,7 @@ export class Arena {
|
||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
|
||||
}
|
||||
|
||||
this.weather = weather ? new Weather(weather, weatherDuration.value) : null;
|
||||
this.weather = weather === WeatherType.NONE ? null : new Weather(weather, weatherDuration.value);
|
||||
this.eventTarget.dispatchEvent(
|
||||
new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!),
|
||||
); // TODO: is this bang correct?
|
||||
@ -404,25 +403,24 @@ export class Arena {
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns whether or not the terrain can be set to {@linkcode terrain} */
|
||||
/** Return whether or not the terrain can be set to {@linkcode terrain} */
|
||||
canSetTerrain(terrain: TerrainType): boolean {
|
||||
return !(this.terrain?.terrainType === (terrain || undefined));
|
||||
return this.getTerrainType() !== terrain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set a new terrain effect to the battle
|
||||
* @param terrain {@linkcode TerrainType} new {@linkcode TerrainType} to set
|
||||
* @param ignoreAnim boolean if the terrain animation should be ignored
|
||||
* @param user {@linkcode Pokemon} that caused the terrain effect
|
||||
* @returns true if new terrain set, false if no terrain provided or attempting to set the same terrain as currently in use
|
||||
* Attempt to set the current terrain to the specified type.
|
||||
* @param terrain - The {@linkcode TerrainType} to try and set.
|
||||
* @param ignoreAnim - Whether to prevent showing an the animation; default `false`
|
||||
* @param user - The {@linkcode Pokemon} creating the terrain (if any)
|
||||
* @returns Whether the terrain was successfully set.
|
||||
*/
|
||||
trySetTerrain(terrain: TerrainType, ignoreAnim = false, user?: Pokemon): boolean {
|
||||
if (!this.canSetTerrain(terrain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldTerrainType = this.terrain?.terrainType || TerrainType.NONE;
|
||||
|
||||
const oldTerrainType = this.getTerrainType();
|
||||
const terrainDuration = new NumberHolder(0);
|
||||
|
||||
if (!isNullOrUndefined(user)) {
|
||||
@ -430,7 +428,7 @@ export class Arena {
|
||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
|
||||
}
|
||||
|
||||
this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null;
|
||||
this.terrain = terrain === TerrainType.NONE ? null : new Terrain(terrain, terrainDuration.value);
|
||||
|
||||
this.eventTarget.dispatchEvent(
|
||||
new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!),
|
||||
@ -464,6 +462,24 @@ export class Arena {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Attempt to override the terrain to the value set inside {@linkcode Overrides.STARTING_TERRAIN_OVERRIDE}. */
|
||||
tryOverrideTerrain(): void {
|
||||
const terrain = Overrides.STARTING_TERRAIN_OVERRIDE;
|
||||
if (terrain === TerrainType.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Add a flag for permanent terrains
|
||||
this.terrain = new Terrain(terrain, 0);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"CommonAnimPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
CommonAnim.MISTY_TERRAIN + (terrain - 1),
|
||||
);
|
||||
globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain) ?? ""); // TODO: Remove `?? ""` when terrain-fail-msg branch removes `null` from these signatures
|
||||
}
|
||||
|
||||
public isMoveWeatherCancelled(user: Pokemon, move: Move): boolean {
|
||||
return !!this.weather && !this.weather.isEffectSuppressed() && this.weather.isMoveWeatherCancelled(user, move);
|
||||
}
|
||||
|
@ -2282,13 +2282,29 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.teraType;
|
||||
}
|
||||
|
||||
public isGrounded(): boolean {
|
||||
/**
|
||||
* Return whether this Pokemon is currently on the ground.
|
||||
*
|
||||
* To be considered grounded, a Pokemon must either:
|
||||
* * Be {@linkcode GroundedTag | forcibly grounded} from an effect like Smack Down or Ingrain
|
||||
* * Be under the effects of {@linkcode ArenaTagType.GRAVITY | harsh gravity}
|
||||
* * **Not** be all of the following things:
|
||||
* * {@linkcode PokemonType.FLYING | Flying-type}
|
||||
* * {@linkcode AbilityId.LEVITATE | Levitating}
|
||||
* * {@linkcode BattlerTagType.FLOATING | Floating} from Magnet Rise or Telekinesis.
|
||||
* * {@linkcode SemiInvulnerableTag | Semi-invulnerable} with `ignoreSemiInvulnerable` set to `false`
|
||||
* @param ignoreSemiInvulnerable - Whether to ignore the target's semi-invulnerable state when determining groundedness;
|
||||
default `false`
|
||||
* @returns Whether this pokemon is currently grounded, as described above.
|
||||
*/
|
||||
public isGrounded(ignoreSemiInvulnerable = false): boolean {
|
||||
return (
|
||||
!!this.getTag(GroundedTag) ||
|
||||
globalScene.arena.hasTag(ArenaTagType.GRAVITY) ||
|
||||
(!this.isOfType(PokemonType.FLYING, true, true) &&
|
||||
!this.hasAbility(AbilityId.LEVITATE) &&
|
||||
!this.getTag(BattlerTagType.FLOATING) &&
|
||||
!this.getTag(SemiInvulnerableTag))
|
||||
(ignoreSemiInvulnerable || !this.getTag(SemiInvulnerableTag)))
|
||||
);
|
||||
}
|
||||
|
||||
@ -2488,7 +2504,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// Handle flying v ground type immunity without removing flying type so effective types are still effective
|
||||
// Related to https://github.com/pagefaultgames/pokerogue/issues/524
|
||||
if (moveType === PokemonType.GROUND && (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY))) {
|
||||
if (moveType === PokemonType.GROUND && this.isGrounded()) {
|
||||
const flyingIndex = types.indexOf(PokemonType.FLYING);
|
||||
if (flyingIndex > -1) {
|
||||
types.splice(flyingIndex, 1);
|
||||
@ -3755,6 +3771,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||
|
||||
/** Combined damage multiplier from field effects such as weather, terrain, etc. */
|
||||
// TODO: This should be applied directly to base power
|
||||
const arenaAttackTypeMultiplier = new NumberHolder(
|
||||
globalScene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()),
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { type PokeballCounts } from "#app/battle-scene";
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
||||
import { Gender } from "#data/gender";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
@ -61,6 +62,12 @@ class DefaultOverrides {
|
||||
readonly SEED_OVERRIDE: string = "";
|
||||
readonly DAILY_RUN_SEED_OVERRIDE: string | null = null;
|
||||
readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
|
||||
/**
|
||||
* If set, will override the in-game terrain at the start of each biome transition.
|
||||
*
|
||||
* Lasts until cleared or replaced by another effect, and is refreshed at the start of each new biome.
|
||||
*/
|
||||
readonly STARTING_TERRAIN_OVERRIDE: TerrainType = TerrainType.NONE;
|
||||
/**
|
||||
* If `null`, ignore this override.
|
||||
*
|
||||
|
@ -253,8 +253,11 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
|
||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
|
||||
globalScene.ui.showText(
|
||||
i18next.t("battle:pokemonCaught", {
|
||||
i18next.t(addStatus.value ? "battle:pokemonCaught" : "battle:pokemonCaughtButChallenge", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
null,
|
||||
@ -290,8 +293,6 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
if (!addStatus.value) {
|
||||
removePokemon();
|
||||
end();
|
||||
|
@ -690,6 +690,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
trySetWeatherIfNewBiome(): void {
|
||||
if (!this.loaded) {
|
||||
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
|
||||
globalScene.arena.tryOverrideTerrain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,10 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
|
||||
globalScene.resetSeed();
|
||||
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentBiome = globalScene.arena.biomeType;
|
||||
const nextWaveIndex = globalScene.currentBattle.waveIndex + 1;
|
||||
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||
const nextWaveIndex = currentWaveIndex + 1;
|
||||
|
||||
const setNextBiome = (nextBiome: BiomeId) => {
|
||||
if (nextWaveIndex % 10 === 1) {
|
||||
@ -26,6 +28,15 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||
if (healStatus.value) {
|
||||
globalScene.phaseManager.unshiftNew("PartyHealPhase", false);
|
||||
} else {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"SelectModifierPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
gameMode.isFixedBattle(currentWaveIndex)
|
||||
? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome);
|
||||
@ -33,12 +44,12 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
};
|
||||
|
||||
if (
|
||||
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||
(globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||
(gameMode.isClassic && gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||
(gameMode.isDaily && gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||
(gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||
) {
|
||||
setNextBiome(BiomeId.END);
|
||||
} else if (globalScene.gameMode.hasRandomBiomes) {
|
||||
} else if (gameMode.hasRandomBiomes) {
|
||||
setNextBiome(this.generateNextBiome(nextWaveIndex));
|
||||
} else if (Array.isArray(biomeLinks[currentBiome])) {
|
||||
const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[])
|
||||
@ -73,9 +84,6 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
generateNextBiome(waveIndex: number): BiomeId {
|
||||
if (!(waveIndex % 50)) {
|
||||
return BiomeId.END;
|
||||
}
|
||||
return globalScene.generateRandomBiome(waveIndex);
|
||||
return waveIndex % 50 === 0 ? BiomeId.END : globalScene.generateRandomBiome(waveIndex);
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,9 @@ import { globalScene } from "#app/global-scene";
|
||||
import { modifierTypes } from "#data/data-lists";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { ChallengeType } from "#enums/challenge-type";
|
||||
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
||||
import type { CustomModifierSettings } from "#modifiers/modifier-type";
|
||||
import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { BooleanHolder } from "#utils/common";
|
||||
|
||||
export class VictoryPhase extends PokemonPhase {
|
||||
public readonly phaseName = "VictoryPhase";
|
||||
@ -49,15 +45,19 @@ export class VictoryPhase extends PokemonPhase {
|
||||
if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
|
||||
globalScene.phaseManager.pushNew("TrainerVictoryPhase");
|
||||
}
|
||||
if (globalScene.gameMode.isEndless || !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
||||
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||
|
||||
if (gameMode.isEndless || !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||
globalScene.phaseManager.pushNew("EggLapsePhase");
|
||||
if (globalScene.gameMode.isClassic) {
|
||||
switch (globalScene.currentBattle.waveIndex) {
|
||||
if (gameMode.isClassic) {
|
||||
switch (currentWaveIndex) {
|
||||
case ClassicFixedBossWaves.RIVAL_1:
|
||||
case ClassicFixedBossWaves.RIVAL_2:
|
||||
// Get event modifiers for this wave
|
||||
timedEventManager
|
||||
.getFixedBattleEventRewards(globalScene.currentBattle.waveIndex)
|
||||
.getFixedBattleEventRewards(currentWaveIndex)
|
||||
.map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r]));
|
||||
break;
|
||||
case ClassicFixedBossWaves.EVIL_BOSS_2:
|
||||
@ -66,59 +66,53 @@ export class VictoryPhase extends PokemonPhase {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0);
|
||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||
if (!healStatus.value) {
|
||||
if (currentWaveIndex % 10) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"SelectModifierPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
this.getFixedBattleCustomModifiers(),
|
||||
gameMode.isFixedBattle(currentWaveIndex)
|
||||
? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings
|
||||
: undefined,
|
||||
);
|
||||
} else if (globalScene.gameMode.isDaily) {
|
||||
} else if (gameMode.isDaily) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM);
|
||||
if (
|
||||
globalScene.currentBattle.waveIndex > 10 &&
|
||||
!globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)
|
||||
) {
|
||||
if (currentWaveIndex > 10 && !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||
}
|
||||
} else {
|
||||
const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||
if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) {
|
||||
const superExpWave = !gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||
if (gameMode.isEndless && currentWaveIndex === 10) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE);
|
||||
}
|
||||
if (
|
||||
globalScene.currentBattle.waveIndex <= 750 &&
|
||||
(globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave)
|
||||
) {
|
||||
if (currentWaveIndex <= 750 && (currentWaveIndex <= 500 || currentWaveIndex % 30 === superExpWave)) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"ModifierRewardPhase",
|
||||
globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250
|
||||
currentWaveIndex % 30 !== superExpWave || currentWaveIndex > 250
|
||||
? modifierTypes.EXP_CHARM
|
||||
: modifierTypes.SUPER_EXP_CHARM,
|
||||
);
|
||||
}
|
||||
if (globalScene.currentBattle.waveIndex <= 150 && !(globalScene.currentBattle.waveIndex % 50)) {
|
||||
if (currentWaveIndex <= 150 && !(currentWaveIndex % 50)) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||
}
|
||||
if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) {
|
||||
if (gameMode.isEndless && !(currentWaveIndex % 50)) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"ModifierRewardPhase",
|
||||
!(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||
!(currentWaveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||
);
|
||||
globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase");
|
||||
}
|
||||
}
|
||||
|
||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
if (gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
globalScene.phaseManager.pushNew("SelectBiomePhase");
|
||||
}
|
||||
|
||||
globalScene.phaseManager.pushNew("NewBattlePhase");
|
||||
} else {
|
||||
globalScene.currentBattle.battleType = BattleType.CLEAR;
|
||||
globalScene.score += globalScene.gameMode.getClearScoreBonus();
|
||||
globalScene.score += gameMode.getClearScoreBonus();
|
||||
globalScene.updateScoreText();
|
||||
globalScene.phaseManager.pushNew("GameOverPhase", true);
|
||||
}
|
||||
@ -126,18 +120,4 @@ export class VictoryPhase extends PokemonPhase {
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* If this wave is a fixed battle with special custom modifier rewards,
|
||||
* will pass those settings to the upcoming {@linkcode SelectModifierPhase}`.
|
||||
*/
|
||||
getFixedBattleCustomModifiers(): CustomModifierSettings | undefined {
|
||||
const gameMode = globalScene.gameMode;
|
||||
const waveIndex = globalScene.currentBattle.waveIndex;
|
||||
if (gameMode.isFixedBattle(waveIndex)) {
|
||||
return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common";
|
||||
|
||||
export class WeatherEffectPhase extends CommonAnimPhase {
|
||||
public readonly phaseName = "WeatherEffectPhase";
|
||||
public weather: Weather | null;
|
||||
public weather: Weather | null; // TODO: This should not be `null`
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
|
@ -448,6 +448,8 @@ export function getAchievementDescription(localizationKey: string): string {
|
||||
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
|
||||
case "FLIP_INVERSE":
|
||||
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
|
||||
case "NUZLOCKE":
|
||||
return i18next.t("achv:NUZLOCKE.description", { context: genderStr });
|
||||
case "BREEDERS_IN_SPACE":
|
||||
return i18next.t("achv:BREEDERS_IN_SPACE.description", {
|
||||
context: genderStr,
|
||||
|
@ -208,6 +208,26 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
);
|
||||
this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant);
|
||||
this.add(this.icon);
|
||||
|
||||
[
|
||||
this.hiddenAbilityIcon,
|
||||
this.favoriteIcon,
|
||||
this.classicWinIcon,
|
||||
this.candyUpgradeIcon,
|
||||
this.candyUpgradeOverlayIcon,
|
||||
this.eggMove1Icon,
|
||||
this.tmMove1Icon,
|
||||
this.eggMove2Icon,
|
||||
this.tmMove2Icon,
|
||||
this.passive1Icon,
|
||||
this.passive2Icon,
|
||||
this.passive1OverlayIcon,
|
||||
this.passive2OverlayIcon,
|
||||
].forEach(icon => {
|
||||
if (icon) {
|
||||
this.bringToTop(icon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkIconId(female, formIndex, shiny, variant) {
|
||||
|
@ -410,6 +410,11 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
new DropDownLabel(i18next.t("filterBar:hasHiddenAbility"), undefined, DropDownState.ON),
|
||||
new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE),
|
||||
];
|
||||
const seenSpeciesLabels = [
|
||||
new DropDownLabel(i18next.t("filterBar:seenSpecies"), undefined, DropDownState.OFF),
|
||||
new DropDownLabel(i18next.t("filterBar:isSeen"), undefined, DropDownState.ON),
|
||||
new DropDownLabel(i18next.t("filterBar:isUnseen"), undefined, DropDownState.EXCLUDE),
|
||||
];
|
||||
const eggLabels = [
|
||||
new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF),
|
||||
new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON),
|
||||
@ -423,6 +428,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
new DropDownOption("FAVORITE", favoriteLabels),
|
||||
new DropDownOption("WIN", winLabels),
|
||||
new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels),
|
||||
new DropDownOption("SEEN_SPECIES", seenSpeciesLabels),
|
||||
new DropDownOption("EGG", eggLabels),
|
||||
new DropDownOption("POKERUS", pokerusLabels),
|
||||
];
|
||||
@ -792,13 +798,15 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
|
||||
}
|
||||
|
||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean {
|
||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry, seenFilter?: boolean): boolean {
|
||||
if (dexEntry?.seenAttr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||
return !!starterDexEntry?.caughtAttr;
|
||||
if (!seenFilter) {
|
||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||
return !!starterDexEntry?.caughtAttr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1617,6 +1625,21 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
}
|
||||
});
|
||||
|
||||
// Seen Filter
|
||||
const dexEntry = globalScene.gameData.dexData[species.speciesId];
|
||||
const isItSeen = this.isSeen(species, dexEntry, true);
|
||||
const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) {
|
||||
return isItSeen;
|
||||
}
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.EXCLUDE) {
|
||||
return !isItSeen;
|
||||
}
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.OFF) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Egg Purchasable Filter
|
||||
const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId);
|
||||
const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||
@ -1658,6 +1681,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
fitsFavorite &&
|
||||
fitsWin &&
|
||||
fitsHA &&
|
||||
fitsSeen &&
|
||||
fitsEgg &&
|
||||
fitsPokerus
|
||||
) {
|
||||
|
@ -210,7 +210,8 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
this.runContainer.add(headerText);
|
||||
const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW);
|
||||
runName.setOrigin(0, 0);
|
||||
runName.setPositionRelative(headerBg, 60, 4);
|
||||
const runNameX = headerText.width / 6 + headerText.x + 4;
|
||||
runName.setPositionRelative(headerBg, runNameX, 4);
|
||||
this.runContainer.add(runName);
|
||||
}
|
||||
|
||||
|
@ -278,27 +278,6 @@ describe("Abilities - Parental Bond", () => {
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
|
||||
});
|
||||
|
||||
it("Smack Down boosted by this ability should only ground the target after the second hit", async () => {
|
||||
game.override.moveset([MoveId.SMACK_DOWN]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.SMACK_DOWN);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to("DamageAnimPhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
});
|
||||
|
||||
it("U-turn boosted by this ability should strike twice before forcing a switch", async () => {
|
||||
game.override.moveset([MoveId.U_TURN]);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Status } from "#data/status-effect";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
@ -104,27 +105,26 @@ describe("Abilities - SHIELDS DOWN", () => {
|
||||
expect(game.field.getPlayerPokemon().status).toBe(undefined);
|
||||
});
|
||||
|
||||
// toxic spikes currently does not poison flying types when gravity is in effect
|
||||
test.todo("should become poisoned by toxic spikes when grounded", async () => {
|
||||
game.override
|
||||
.enemyMoveset([MoveId.GRAVITY, MoveId.TOXIC_SPIKES, MoveId.SPLASH])
|
||||
.moveset([MoveId.GRAVITY, MoveId.SPLASH]);
|
||||
|
||||
it("should be poisoned by toxic spikes when Gravity is active before changing forms", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]);
|
||||
|
||||
// turn 1
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.move.selectEnemyMove(MoveId.TOXIC_SPIKES);
|
||||
// Change minior to core form in a state where it would revert on switch
|
||||
const minior = game.scene.getPlayerParty()[1];
|
||||
minior.formIndex = redCoreForm;
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC_SPIKES);
|
||||
await game.toNextTurn();
|
||||
|
||||
// turn 2
|
||||
expect(game).toHaveArenaTag(ArenaTagType.GRAVITY);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.MINIOR);
|
||||
expect(game.field.getPlayerPokemon().species.formIndex).toBe(0);
|
||||
expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.POISON);
|
||||
expect(minior.species.speciesId).toBe(SpeciesId.MINIOR);
|
||||
expect(minior.formIndex).toBe(0);
|
||||
expect(minior.isGrounded()).toBe(true);
|
||||
expect(minior).toHaveStatusEffect(StatusEffect.POISON);
|
||||
});
|
||||
|
||||
test("should ignore yawn", async () => {
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
@ -27,130 +25,49 @@ describe("Arena - Gravity", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.moveset([MoveId.TACKLE, MoveId.GRAVITY, MoveId.FISSURE])
|
||||
.ability(AbilityId.UNNERVE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyLevel(5);
|
||||
});
|
||||
|
||||
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
||||
|
||||
it("non-OHKO move accuracy is multiplied by 1.67", async () => {
|
||||
const moveToCheck = allMoves[MoveId.TACKLE];
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
||||
|
||||
// Setup Gravity on first turn
|
||||
it("should multiply all non-OHKO move accuracy by 1.67x", async () => {
|
||||
const accSpy = vi.spyOn(allMoves[MoveId.TACKLE], "calculateBattleAccuracy");
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use non-OHKO move on second turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
|
||||
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67);
|
||||
});
|
||||
|
||||
it("OHKO move accuracy is not affected", async () => {
|
||||
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
|
||||
const moveToCheck = allMoves[MoveId.FISSURE];
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
||||
|
||||
// Setup Gravity on first turn
|
||||
it("should not affect OHKO move accuracy", async () => {
|
||||
const accSpy = vi.spyOn(allMoves[MoveId.FISSURE], "calculateBattleAccuracy");
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.move.forceEnemyMove(MoveId.FISSURE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use OHKO move on second turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.FISSURE);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
|
||||
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.FISSURE].accuracy);
|
||||
});
|
||||
|
||||
describe("Against flying types", () => {
|
||||
it("can be hit by ground-type moves now", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.EARTHQUAKE]);
|
||||
it("should forcibly ground all Pokemon for the duration of the effect", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
const pidgeot = game.field.getEnemyPokemon();
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
|
||||
// Try earthquake on 1st turn (fails!);
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
|
||||
|
||||
// Setup Gravity on 2nd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use ground move on 3rd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
|
||||
});
|
||||
|
||||
it("keeps super-effective moves super-effective after using gravity", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.THUNDERBOLT]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
const pidgeot = game.field.getEnemyPokemon();
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
|
||||
// Setup Gravity on 1st turn
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use electric move on 2nd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.THUNDERBOLT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels Fly if its user is semi-invulnerable", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX).enemyMoveset(MoveId.FLY).moveset([MoveId.GRAVITY, MoveId.SPLASH]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||
|
||||
const charizard = game.field.getPlayerPokemon();
|
||||
const snorlax = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
|
||||
|
||||
game.move.select(MoveId.GRAVITY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
expect(player.isGrounded()).toBe(true);
|
||||
expect(enemy.isGrounded()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -1,69 +0,0 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Arena - Grassy Terrain", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(1)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.enemyMoveset(MoveId.FLY)
|
||||
.moveset([MoveId.GRASSY_TERRAIN, MoveId.EARTHQUAKE])
|
||||
.ability(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("halves the damage of Earthquake", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
||||
|
||||
const eq = allMoves[MoveId.EARTHQUAKE];
|
||||
vi.spyOn(eq, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(eq.calculateBattlePower).toHaveReturnedWith(100);
|
||||
|
||||
game.move.select(MoveId.GRASSY_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(eq.calculateBattlePower).toHaveReturnedWith(50);
|
||||
});
|
||||
|
||||
it("Does not halve the damage of Earthquake if opponent is not grounded", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.NINJASK]);
|
||||
|
||||
const eq = allMoves[MoveId.EARTHQUAKE];
|
||||
vi.spyOn(eq, "calculateBattlePower");
|
||||
|
||||
game.move.select(MoveId.GRASSY_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(eq.calculateBattlePower).toHaveReturnedWith(100);
|
||||
});
|
||||
});
|
@ -1,59 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Arena - Psychic Terrain", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(1)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.moveset([MoveId.PSYCHIC_TERRAIN, MoveId.RAIN_DANCE, MoveId.DARK_VOID])
|
||||
.ability(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
it("Dark Void with Prankster is not blocked", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.PSYCHIC_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.DARK_VOID);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("Rain Dance with Prankster is not blocked", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.PSYCHIC_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.RAIN_DANCE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN);
|
||||
});
|
||||
});
|
400
test/arena/terrain.test.ts
Normal file
400
test/arena/terrain.test.ts
Normal file
@ -0,0 +1,400 @@
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { getTerrainName, TerrainType } from "#app/data/terrain";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { randSeedInt } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Terrain -", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.passiveAbility(AbilityId.NO_GUARD);
|
||||
});
|
||||
|
||||
// TODO: Terrain boosts currently apply to damage dealt, not base power
|
||||
describe.todo.each<{ name: string; type: PokemonType; terrain: TerrainType; move: MoveId }>([
|
||||
{ name: "Electric", type: PokemonType.ELECTRIC, terrain: TerrainType.ELECTRIC, move: MoveId.THUNDERBOLT },
|
||||
{ name: "Psychic", type: PokemonType.PSYCHIC, terrain: TerrainType.PSYCHIC, move: MoveId.PSYCHIC },
|
||||
{ name: "Grassy", type: PokemonType.GRASS, terrain: TerrainType.GRASSY, move: MoveId.ENERGY_BALL },
|
||||
{ name: "Misty", type: PokemonType.FAIRY, terrain: TerrainType.MISTY, move: MoveId.DRAGON_BREATH },
|
||||
])("Common Tests - $name Terrain", ({ type, terrain, move }) => {
|
||||
// biome-ignore lint/suspicious/noDuplicateTestHooks: This is a TODO test case
|
||||
beforeEach(() => {
|
||||
game.override.terrain(terrain).enemyPassiveAbility(AbilityId.LEVITATE);
|
||||
});
|
||||
|
||||
const typeStr = toTitleCase(PokemonType[type]);
|
||||
|
||||
it.skipIf(terrain === TerrainType.MISTY)(
|
||||
`should boost power of grounded ${typeStr}-type moves by 1.3x, even against ungrounded targets`,
|
||||
async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Player grounded attack got boosted while enemy ungrounded attack didn't
|
||||
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 1.3);
|
||||
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power);
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(terrain === TerrainType.MISTY)(
|
||||
"should cut power of grounded Dragon-type moves in half, even from ungrounded users",
|
||||
async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Enemy dragon breath got nerfed against grounded player; player dragon breath did not
|
||||
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power);
|
||||
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power * 0.5);
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: Move to a dedicated terrain pulse test file
|
||||
it(`should change Terrain Pulse into a ${typeStr}-type move and double its base power`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[MoveId.TERRAIN_PULSE], "calculateBattlePower");
|
||||
const playerTypeSpy = vi.spyOn(game.field.getPlayerPokemon(), "getMoveType");
|
||||
const enemyTypeSpy = vi.spyOn(game.field.getEnemyPokemon(), "getMoveType");
|
||||
|
||||
game.move.use(MoveId.TERRAIN_PULSE);
|
||||
await game.move.forceEnemyMove(MoveId.TERRAIN_PULSE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// player grounded terrain pulse was boosted & type converted; enemy ungrounded one wasn't
|
||||
expect(powerSpy).toHaveLastReturnedWith(
|
||||
allMoves[MoveId.TERRAIN_PULSE].power * (terrain === TerrainType.MISTY ? 2 : 2.6),
|
||||
); // 2 * 1.3
|
||||
expect(playerTypeSpy).toHaveLastReturnedWith(type);
|
||||
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].power);
|
||||
expect(enemyTypeSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].type);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Grassy Terrain", () => {
|
||||
beforeEach(() => {
|
||||
game.override.terrain(TerrainType.GRASSY);
|
||||
});
|
||||
|
||||
it("should heal all grounded, non semi-invulnerable Pokemon for 1/16th max HP at end of turn", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
// blissey is grounded, shuckle isn't
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp /= 2;
|
||||
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
game.field.mockAbility(shuckle, AbilityId.LEVITATE);
|
||||
shuckle.hp /= 2;
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
||||
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
|
||||
game.move.use(MoveId.DIG);
|
||||
await game.toNextTurn();
|
||||
|
||||
// shuckle is airborne and blissey is semi-invulnerable, so nobody gets healed
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
||||
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
});
|
||||
|
||||
// TODO: Enable once magnitude to return a specific power rating
|
||||
it.todo.each<{ name: string; move: MoveId; basePower?: number }>([
|
||||
{ name: "Bulldoze", move: MoveId.BULLDOZE },
|
||||
{ name: "Earthquake", move: MoveId.EARTHQUAKE },
|
||||
{ name: "Magnitude", move: MoveId.MAGNITUDE, basePower: 150 }, // magnitude 10
|
||||
])(
|
||||
"should halve $name's base power against grounded, on-field targets",
|
||||
async ({ move, basePower = allMoves[move].power }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
||||
// force high rolls for guaranteed magnitude 10s
|
||||
vi.fn(randSeedInt).mockReturnValue(100);
|
||||
|
||||
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
// Turn 1: attack with grassy terrain active; 0.5x
|
||||
game.move.use(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower / 2);
|
||||
|
||||
// Turn 2: Give enemy levitate to make ungrounded and attack; 1x
|
||||
// (hits due to no guard)
|
||||
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.LEVITATE);
|
||||
game.move.use(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||
|
||||
// Turn 3: Remove levitate and make enemy semi-invulnerable; 1x
|
||||
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.BALL_FETCH);
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(MoveId.FLY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getLastXMoves()[0].move).toBe(MoveId.FLY);
|
||||
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Electric Terrain", () => {
|
||||
beforeEach(() => {
|
||||
game.override.terrain(TerrainType.ELECTRIC);
|
||||
});
|
||||
|
||||
it("should prevent all grounded Pokemon from being put to sleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const pidgeot = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
expect(pidgeot.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(shuckle.status?.effect).toBeUndefined();
|
||||
// TODO: These don't work due to how move failures are propagated
|
||||
// expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
// expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent attack moves from applying sleep without showing text/failing move", async () => {
|
||||
vi.spyOn(allMoves[MoveId.RELIC_SONG], "chance", "get").mockReturnValue(100);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
const statusSpy = vi.spyOn(shuckle, "canSetStatus");
|
||||
|
||||
game.move.use(MoveId.RELIC_SONG);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
expect(shuckle.status?.effect).toBeUndefined();
|
||||
expect(statusSpy).toHaveLastReturnedWith(false);
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Misty Terrain", () => {
|
||||
beforeEach(() => {
|
||||
game.override.terrain(TerrainType.MISTY).enemyPassiveAbility(AbilityId.LEVITATE);
|
||||
});
|
||||
|
||||
it("should prevent all grounded Pokemon from gaining non-volatile statuses", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(MoveId.TOXIC);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC);
|
||||
await game.toNextTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
// blissey is grounded & protected, shuckle isn't
|
||||
expect(blissey.status?.effect).toBeUndefined();
|
||||
expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
// TODO: These don't work due to how move failures are propagated
|
||||
// expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
// expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should block confusion and display message", async () => {
|
||||
game.override.confusionActivation(false); // prevent self hits from cancelling move
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(MoveId.CONFUSE_RAY);
|
||||
await game.move.forceEnemyMove(MoveId.CONFUSE_RAY);
|
||||
await game.toNextTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
// blissey is grounded & protected, shuckle isn't
|
||||
expect(blissey).not.toHaveBattlerTag(BattlerTagType.CONFUSED);
|
||||
expect(shuckle).toHaveBattlerTag(BattlerTagType.CONFUSED);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each<{ status: string; move: MoveId }>([
|
||||
{ status: "Sleep", move: MoveId.RELIC_SONG },
|
||||
{ status: "Burn", move: MoveId.SACRED_FIRE },
|
||||
{ status: "Freeze", move: MoveId.ICE_BEAM },
|
||||
{ status: "Paralysis", move: MoveId.NUZZLE },
|
||||
{ status: "Poison", move: MoveId.SLUDGE_BOMB },
|
||||
{ status: "Toxic Poison", move: MoveId.MALIGNANT_CHAIN },
|
||||
// TODO: Confusion currently displays terrain block message even from damaging moves
|
||||
// { status: "Confusion", move: MoveId.MAGICAL_TORQUE },
|
||||
])("should prevent attack moves from applying $status without showing text/failing move", async ({ move }) => {
|
||||
vi.spyOn(allMoves[move], "chance", "get").mockReturnValue(100);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
// Blissey was grounded and protected from effect, but still took damage
|
||||
expect(blissey).not.toHaveFullHp();
|
||||
expect(blissey).not.toHaveBattlerTag(BattlerTagType.CONFUSED);
|
||||
expect(blissey.status?.effect).toBe(StatusEffect.NONE);
|
||||
expect(shuckle).toHaveUsedMove({ result: MoveResult.SUCCESS });
|
||||
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Psychic Terrain", () => {
|
||||
beforeEach(() => {
|
||||
game.override.terrain(TerrainType.PSYCHIC).ability(AbilityId.GALE_WINGS).enemyAbility(AbilityId.PRANKSTER);
|
||||
});
|
||||
|
||||
it("should block all opponent-targeted priority moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(MoveId.FAKE_OUT);
|
||||
await game.move.forceEnemyMove(MoveId.FOLLOW_ME);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.PSYCHIC),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should affect moves that only become priority due to abilities", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(MoveId.FEATHER_DANCE);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||
terrainName: getTerrainName(TerrainType.PSYCHIC),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each<{ category: string; move: MoveId; effect: () => void }>([
|
||||
{
|
||||
category: "Field-targeted",
|
||||
move: MoveId.RAIN_DANCE,
|
||||
effect: () => {
|
||||
expect(game.scene.arena.getWeatherType()).toBe(WeatherType.RAIN);
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "Enemy-targeting spread",
|
||||
move: MoveId.DARK_VOID,
|
||||
effect: () => {
|
||||
expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
||||
},
|
||||
},
|
||||
])("should not block $category moves that become priority", async ({ move, effect }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
effect();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,8 +1,10 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -52,4 +54,18 @@ describe("Challenges - Limited Catch", () => {
|
||||
|
||||
expect(game.scene.getPlayerParty()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should allow gift Pokémon from Mystery Encounters to be added to party", async () => {
|
||||
game.override
|
||||
.mysteryEncounterChance(100)
|
||||
.mysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN)
|
||||
.startingWave(12);
|
||||
game.scene.money = 20000;
|
||||
|
||||
await game.challengeMode.runToSummon([SpeciesId.NUZLEAF]);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
|
||||
expect(game.scene.getPlayerParty()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { ExpBoosterModifier } from "#modifiers/modifier";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
||||
import Phaser from "phaser";
|
||||
@ -75,6 +76,7 @@ describe("Challenges - Limited Support", () => {
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
expect(game.scene.getModifiers(ExpBoosterModifier)).toHaveLength(1);
|
||||
expect(playerPokemon).not.toHaveFullHp();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
|
132
test/moves/fly-bounce.test.ts
Normal file
132
test/moves/fly-bounce.test.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Fly and Bounce", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.ability(AbilityId.COMPOUND_EYES)
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.TACKLE);
|
||||
});
|
||||
|
||||
// TODO: Move to a global "charging moves" test file
|
||||
it.each([
|
||||
{ name: "Fly", move: MoveId.FLY },
|
||||
{ name: "Bounce", move: MoveId.BOUNCE },
|
||||
])("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.use(MoveId.FLY);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
expect(player).toHaveBattlerTag(BattlerTagType.FLYING);
|
||||
expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
expect(player.hp).toBe(player.getMaxHp());
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(player.getMoveQueue()[0].move).toBe(MoveId.FLY);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(player).not.toHaveBattlerTag(BattlerTagType.FLYING);
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(player.getMoveHistory()).toHaveLength(2);
|
||||
|
||||
const playerFly = player.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
// TODO: Move to a No Guard test file
|
||||
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
||||
game.override.enemyAbility(AbilityId.NO_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.FLY);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not expend PP when the attack phase is cancelled", async () => {
|
||||
game.override.enemyAbility(AbilityId.NO_GUARD).enemyMoveset(MoveId.SPORE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.use(MoveId.FLY);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(playerPokemon).not.toHaveBattlerTag(BattlerTagType.FLYING);
|
||||
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(0);
|
||||
});
|
||||
|
||||
// TODO: We currently cancel Fly/Bounce in a really scuffed way
|
||||
it.todo.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Smack Down", move: MoveId.SMACK_DOWN },
|
||||
{ name: "Thousand Arrows", move: MoveId.THOUSAND_ARROWS },
|
||||
{ name: "Gravity", move: MoveId.GRAVITY },
|
||||
])("should be cancelled immediately when $name is used", async ({ move }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.AZURILL]);
|
||||
|
||||
game.move.use(MoveId.BOUNCE);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
// Bounce should've worked until hit
|
||||
const azurill = game.field.getPlayerPokemon();
|
||||
expect(azurill).toHaveBattlerTag(BattlerTagType.FLYING);
|
||||
expect(azurill).not.toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(azurill).not.toHaveBattlerTag(BattlerTagType.FLYING);
|
||||
expect(azurill).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
expect(azurill.getMoveQueue()).toHaveLength(0);
|
||||
expect(azurill.visible).toBe(true);
|
||||
if (move !== MoveId.GRAVITY) {
|
||||
expect(azurill.hp).toBeLessThan(azurill.getMaxHp());
|
||||
}
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const snorlax = game.field.getEnemyPokemon();
|
||||
expect(snorlax.hp).toBe(snorlax.getMaxHp());
|
||||
});
|
||||
});
|
@ -1,120 +0,0 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Fly", () => {
|
||||
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(MoveId.FLY)
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.TACKLE);
|
||||
|
||||
vi.spyOn(allMoves[MoveId.FLY], "accuracy", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveQueue()[0].move).toBe(MoveId.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
||||
game.override.enemyAbility(AbilityId.NO_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not expend PP when the attack phase is cancelled", async () => {
|
||||
game.override.enemyAbility(AbilityId.NO_GUARD).enemyMoveset(MoveId.SPORE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
||||
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(0);
|
||||
});
|
||||
|
||||
it("should be cancelled when another Pokemon uses Gravity", async () => {
|
||||
game.override.enemyMoveset([MoveId.SPLASH, MoveId.GRAVITY]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.FLY);
|
||||
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
|
||||
await game.toNextTurn();
|
||||
await game.move.selectEnemyMove(MoveId.GRAVITY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(0);
|
||||
});
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
@ -28,30 +29,16 @@ describe("Moves - Magnet Rise", () => {
|
||||
.enemyLevel(1);
|
||||
});
|
||||
|
||||
it("should make the user immune to ground-type moves", async () => {
|
||||
it("should make the user ungrounded when used", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGNEZONE]);
|
||||
|
||||
game.move.use(MoveId.MAGNET_RISE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// magnezone levitated and was not hit by earthquake
|
||||
const magnezone = game.field.getPlayerPokemon();
|
||||
expect(magnezone.hp).toBe(magnezone.getMaxHp());
|
||||
expect(magnezone.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
expect(magnezone.isGrounded()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be removed by gravity", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGNEZONE]);
|
||||
|
||||
game.move.use(MoveId.MAGNET_RISE);
|
||||
await game.toNextTurn();
|
||||
|
||||
const magnezone = game.field.getPlayerPokemon();
|
||||
expect(magnezone.hp).toBe(magnezone.getMaxHp());
|
||||
|
||||
game.move.use(MoveId.GRAVITY);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(magnezone.hp).toBeLessThan(magnezone.getMaxHp());
|
||||
expect(magnezone.isGrounded()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
124
test/moves/smack-down-thousand-arrows.test.ts
Normal file
124
test/moves/smack-down-thousand-arrows.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Smack Down and Thousand Arrows", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.EELEKTROSS)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(50)
|
||||
.criticalHits(false)
|
||||
.ability(AbilityId.COMPOUND_EYES)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "Smack Down", move: MoveId.SMACK_DOWN },
|
||||
{ name: "Thousand Arrows", move: MoveId.THOUSAND_ARROWS },
|
||||
])("$name should hit and ground ungrounded targets", async ({ move }) => {
|
||||
game.override.enemySpecies(SpeciesId.TORNADUS);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.isGrounded()).toBe(false);
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
expect(enemy.isGrounded()).toBe(true);
|
||||
});
|
||||
|
||||
it("should affect targets with Levitate", async () => {
|
||||
game.override.enemyPassiveAbility(AbilityId.LEVITATE);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const eelektross = game.field.getEnemyPokemon();
|
||||
expect(eelektross.isGrounded()).toBe(false);
|
||||
|
||||
game.move.use(MoveId.THOUSAND_ARROWS);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(eelektross).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||
expect(eelektross.isGrounded()).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "TELEKINESIS", tag: BattlerTagType.TELEKINESIS },
|
||||
{ name: "FLOATING", tag: BattlerTagType.FLOATING },
|
||||
])("should cancel the effects of BattlerTagType.$name", async ({ tag }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const eelektross = game.field.getEnemyPokemon();
|
||||
eelektross.addTag(tag);
|
||||
|
||||
game.move.use(MoveId.SMACK_DOWN);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(eelektross).not.toHaveBattlerTag(tag);
|
||||
expect(eelektross).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
});
|
||||
|
||||
// NB: This test might sound useless, but semi-invulnerable pokemon are technically considered "ungrounded"
|
||||
// by most things
|
||||
it("should not ground semi-invulnerable targets hit via No Guard unless already ungrounded", async () => {
|
||||
game.override.ability(AbilityId.NO_GUARD);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
game.move.use(MoveId.THOUSAND_ARROWS);
|
||||
await game.move.forceEnemyMove(MoveId.DIG);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Eelektross took damage but was not forcibly grounded
|
||||
const eelektross = game.field.getEnemyPokemon();
|
||||
expect(eelektross.isGrounded()).toBe(true);
|
||||
expect(eelektross).not.toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||
});
|
||||
|
||||
// TODO: Sky drop is currently partially implemented
|
||||
it.todo("should hit midair targets from Sky Drop without interrupting");
|
||||
|
||||
describe("Thousand Arrows", () => {
|
||||
it("should deal a fixed 1x damage to ungrounded flying-types", async () => {
|
||||
game.override.enemySpecies(SpeciesId.ARCHEOPS);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const archeops = game.field.getEnemyPokemon();
|
||||
game.move.use(MoveId.THOUSAND_ARROWS);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(hitSpy).toHaveReturnedWith([expect.anything(), 1]);
|
||||
expect(archeops).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
expect(archeops.isGrounded()).toBe(true);
|
||||
expect(archeops.hp).toBeLessThan(archeops.getMaxHp());
|
||||
});
|
||||
});
|
||||
});
|
@ -1,10 +1,13 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { HitCheckResult } from "#enums/hit-check-result";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -26,114 +29,117 @@ describe("Moves - Telekinesis", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.TELEKINESIS, MoveId.TACKLE, MoveId.MUD_SHOT, MoveId.SMACK_DOWN])
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.SNORLAX)
|
||||
.enemyLevel(60)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset([MoveId.SPLASH]);
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("Telekinesis makes the affected vulnerable to most attacking moves regardless of accuracy", async () => {
|
||||
it("should cause opposing non-OHKO moves to always hit the target", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemyOpponent = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.toNextTurn();
|
||||
vi.spyOn(allMoves[MoveId.TACKLE], "accuracy", "get").mockReturnValue(0);
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.isFullHp()).toBe(false);
|
||||
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||
|
||||
game.move.use(MoveId.TACKLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceMiss();
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expect(player.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("Telekinesis makes the affected airborne and immune to most Ground-moves", async () => {
|
||||
it("should forcibly unground the target", async () => {
|
||||
game.override.terrain(TerrainType.ELECTRIC);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemyOpponent = game.field.getEnemyPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Use Earthquake - should be ineffective
|
||||
game.move.use(MoveId.EARTHQUAKE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||
|
||||
await game.toNextTurn();
|
||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(100);
|
||||
game.move.select(MoveId.MUD_SHOT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.isFullHp()).toBe(true);
|
||||
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
expect(hitSpy).toHaveLastReturnedWith([HitCheckResult.NO_EFFECT, 0]);
|
||||
|
||||
// Use Spore - should succeed due to being ungrounded
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("Telekinesis can still affect Pokemon that have been transformed into invalid Pokemon", async () => {
|
||||
game.override.enemyMoveset(MoveId.TRANSFORM);
|
||||
// TODO: Make an it.each testing the invalid species for Telekinesis
|
||||
it.todo.each([])("should fail if used on $name", () => {});
|
||||
|
||||
it("should still affect enemies transformed into invalid Pokemon", async () => {
|
||||
game.override.enemyAbility(AbilityId.IMPOSTER);
|
||||
await game.classicMode.startBattle([SpeciesId.DIGLETT]);
|
||||
|
||||
const enemyOpponent = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyOpponent).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||
expect(enemyOpponent).toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||
expect(enemyOpponent.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT);
|
||||
});
|
||||
|
||||
it("Moves like Smack Down and 1000 Arrows remove all effects of Telekinesis from the target Pokemon", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemyOpponent = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
|
||||
await game.toNextTurn();
|
||||
game.move.select(MoveId.SMACK_DOWN);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Ingrain will remove the floating effect of Telekinesis, but not the 100% hit", async () => {
|
||||
game.override.enemyMoveset([MoveId.SPLASH, MoveId.INGRAIN]);
|
||||
it("should become grounded when Ingrain is used, but not remove the guaranteed hit effect", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyOpponent = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.TELEKINESIS);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.TELEKINESIS);
|
||||
await game.toNextTurn();
|
||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(0);
|
||||
game.move.select(MoveId.MUD_SHOT);
|
||||
await game.move.selectEnemyMove(MoveId.INGRAIN);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.INGRAIN)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
game.move.use(MoveId.MUD_SHOT);
|
||||
await game.move.forceEnemyMove(MoveId.INGRAIN);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.move.forceMiss();
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.INGRAIN);
|
||||
expect(enemy).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||
expect(enemy).not.toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||
expect(enemy.isGrounded()).toBe(true);
|
||||
expect(playerPokemon).toHaveUsedMove({ move: MoveId.MUD_SHOT, result: MoveResult.SUCCESS });
|
||||
});
|
||||
|
||||
it("should not be baton passed onto a mega gengar", async () => {
|
||||
game.override
|
||||
.moveset([MoveId.BATON_PASS])
|
||||
.enemyMoveset([MoveId.TELEKINESIS])
|
||||
.starterForms({ [SpeciesId.GENGAR]: 1 });
|
||||
|
||||
it("should not be baton passable onto a mega gengar", async () => {
|
||||
game.override.starterForms({ [SpeciesId.GENGAR]: 1 });
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]);
|
||||
game.move.select(MoveId.BATON_PASS);
|
||||
|
||||
game.move.use(MoveId.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.move.forceEnemyMove(MoveId.TELEKINESIS);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.field.getPlayerPokemon().getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(game.field.getPlayerPokemon()).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.field.getPlayerPokemon()).not.toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||
});
|
||||
});
|
||||
|
@ -1,89 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { BerryPhase } from "#phases/berry-phase";
|
||||
import { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Thousand Arrows", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.TOGETIC)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.moveset([MoveId.THOUSAND_ARROWS])
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("move should hit and ground Flying-type targets", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Enemy should not be grounded before move effect is applied
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("move should hit and ground targets with Levitate", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX).enemyAbility(AbilityId.LEVITATE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Enemy should not be grounded before move effect is applied
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("move should hit and ground targets under the effects of Magnet Rise", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SNORLAX);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
enemyPokemon.addTag(BattlerTagType.FLOATING, undefined, MoveId.MAGNET_RISE);
|
||||
|
||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
});
|
@ -1,7 +1,9 @@
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||
import type { NewArenaEvent } from "#events/battle-scene";
|
||||
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
|
||||
import Overrides from "#app/overrides";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
@ -359,6 +361,19 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the starting {@linkcode TerrainType} that will be set on entering a new biome.
|
||||
* @param type - The {@linkcode TerrainType} to set.
|
||||
* @returns `this`
|
||||
* @remarks
|
||||
* The newly added terrain will be refreshed upon reaching a new biome, and will be overridden as normal if a new terrain is set.
|
||||
*/
|
||||
public terrain(type: TerrainType): this {
|
||||
vi.spyOn(Overrides, "STARTING_TERRAIN_OVERRIDE", "get").mockReturnValue(type);
|
||||
this.log(`Starting terrain for next biome set to ${TerrainType[type]} (=${type})!`);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the seed
|
||||
* @param seed - The seed to set
|
||||
|
@ -36,6 +36,7 @@ import { NewBiomeEncounterPhase } from "#phases/new-biome-encounter-phase";
|
||||
import { NextEncounterPhase } from "#phases/next-encounter-phase";
|
||||
import { PartyExpPhase } from "#phases/party-exp-phase";
|
||||
import { PartyHealPhase } from "#phases/party-heal-phase";
|
||||
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
||||
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
||||
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
||||
@ -146,6 +147,7 @@ export class PhaseInterceptor {
|
||||
[PositionalTagPhase, this.startPhase],
|
||||
[PokemonTransformPhase, this.startPhase],
|
||||
[MysteryEncounterPhase, this.startPhase],
|
||||
[PokemonHealPhase, this.startPhase],
|
||||
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
||||
[MysteryEncounterBattlePhase, this.startPhase],
|
||||
[MysteryEncounterRewardsPhase, this.startPhase],
|
||||
|
Loading…
Reference in New Issue
Block a user