mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 22:09: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 {
|
onAdd(_arena: Arena): void {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
|
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
|
||||||
|
|
||||||
|
// Remove all flying-related effects from all on-field Pokemon.
|
||||||
globalScene.getField(true).forEach(pokemon => {
|
globalScene.getField(true).forEach(pokemon => {
|
||||||
if (pokemon !== null) {
|
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
pokemon.removeTag(BattlerTagType.FLYING);
|
||||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
// 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_ARCANINE,
|
||||||
SpeciesId.HISUI_AVALUGG,
|
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]: [
|
[MoveId.WATER_PULSE]: [
|
||||||
SpeciesId.SQUIRTLE,
|
SpeciesId.SQUIRTLE,
|
||||||
SpeciesId.WARTORTLE,
|
SpeciesId.WARTORTLE,
|
||||||
@ -68747,6 +69026,7 @@ export const tmPoolTiers: TmPoolTiers = {
|
|||||||
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA,
|
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA,
|
||||||
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT,
|
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT,
|
||||||
[MoveId.ROCK_BLAST]: ModifierTier.GREAT,
|
[MoveId.ROCK_BLAST]: ModifierTier.GREAT,
|
||||||
|
[MoveId.SHOCK_WAVE]: ModifierTier.GREAT,
|
||||||
[MoveId.WATER_PULSE]: ModifierTier.GREAT,
|
[MoveId.WATER_PULSE]: ModifierTier.GREAT,
|
||||||
[MoveId.ROOST]: ModifierTier.GREAT,
|
[MoveId.ROOST]: ModifierTier.GREAT,
|
||||||
[MoveId.GRAVITY]: ModifierTier.COMMON,
|
[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 {
|
export class InterruptedTag extends BattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.INTERRUPTED;
|
public override readonly tagType = BattlerTagType.INTERRUPTED;
|
||||||
constructor(sourceMove: MoveId) {
|
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 {
|
export class ConfusedTag extends SerializableBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.CONFUSED;
|
public override readonly tagType = BattlerTagType.CONFUSED;
|
||||||
@ -774,8 +778,9 @@ export class ConfusedTag extends SerializableBattlerTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canAdd(pokemon: Pokemon): boolean {
|
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) {
|
if (blockedByTerrain) {
|
||||||
|
// TODO: this should not trigger if the current move is an attacking move
|
||||||
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
|
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1536,6 +1536,7 @@ export class CritOnlyAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Fix subclasses to actually extend from `getDamage`
|
||||||
export class FixedDamageAttr extends MoveAttr {
|
export class FixedDamageAttr extends MoveAttr {
|
||||||
private damage: number;
|
private damage: number;
|
||||||
|
|
||||||
@ -5372,13 +5373,11 @@ export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
if (!target.isGrounded(true) && target.isOfType(PokemonType.FLYING)) {
|
||||||
const multiplier = args[0] as NumberHolder;
|
const multiplier = args[0];
|
||||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
// When a flying type is hit, the first hit is always 1x multiplier.
|
||||||
if (target.isOfType(PokemonType.FLYING)) {
|
multiplier.value = 1;
|
||||||
multiplier.value = 1;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5617,13 +5616,13 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
|||||||
protected cancelOnFail: boolean;
|
protected cancelOnFail: boolean;
|
||||||
private failOnOverlap: 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 });
|
super(selfTarget, { lastHitOnly: lastHitOnly });
|
||||||
|
|
||||||
this.tagType = tagType;
|
this.tagType = tagType;
|
||||||
this.turnCountMin = turnCountMin;
|
this.turnCountMin = turnCountMin;
|
||||||
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
|
this.turnCountMax = turnCountMax;
|
||||||
this.failOnOverlap = !!failOnOverlap;
|
this.failOnOverlap = failOnOverlap;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
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);
|
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||||
if (moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCondition(): MoveConditionFunc | null {
|
getCondition(): MoveConditionFunc | null {
|
||||||
|
// TODO: This should consider whether the tag can be added
|
||||||
return this.failOnOverlap
|
return this.failOnOverlap
|
||||||
? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType)
|
? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType)
|
||||||
: null;
|
: null;
|
||||||
@ -5717,8 +5717,10 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the appropriate battler tag for Smack Down and Thousand arrows
|
* Attribute to add the {@linkcode BattlerTagType.IGNORE_FLYING | IGNORE_FLYING} battler tag to the target
|
||||||
* @extends AddBattlerTagAttr
|
* and remove any prior sources of ungroundedness.
|
||||||
|
*
|
||||||
|
* Does nothing if the target was not already ungrounded.
|
||||||
*/
|
*/
|
||||||
export class FallDownAttr extends AddBattlerTagAttr {
|
export class FallDownAttr extends AddBattlerTagAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -5726,18 +5728,35 @@ export class FallDownAttr extends AddBattlerTagAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds Grounded Tag to the target and checks if fallDown message should be displayed
|
* Add `GroundedTag` to the target, remove all prior sources of ungroundedness
|
||||||
* @param user the {@linkcode Pokemon} using the move
|
* and display a message.
|
||||||
* @param target the {@linkcode Pokemon} targeted by the move
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
* @param move the {@linkcode Move} invoking this effect
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
|
* @param move - The {@linkcode Move} invoking this effect
|
||||||
* @param args n/a
|
* @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 {
|
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
||||||
if (!target.isGrounded()) {
|
// Smack down and similar only add their tag if the target is already ungrounded,
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
// 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 {
|
export class LapseBattlerTagAttr extends MoveEffectAttr {
|
||||||
public tagTypes: BattlerTagType[];
|
public tagTypes: BattlerTagType[];
|
||||||
|
|
||||||
@ -8040,7 +8060,12 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
|
|||||||
return phase.isForcedLast() && slower;
|
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();
|
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 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 attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||||
if (heldItems.length === 0) {
|
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 = {
|
export type MoveTargetSet = {
|
||||||
targets: BattlerIndex[];
|
targets: BattlerIndex[];
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
@ -8901,7 +8927,6 @@ export function initMoves() {
|
|||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
/* Transform:
|
/* Transform:
|
||||||
* Does not copy the target's rage fist hit count
|
* 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)
|
* Renders user typeless when copying typeless opponent (should revert to original typing)
|
||||||
*/
|
*/
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
@ -9356,6 +9381,7 @@ export function initMoves() {
|
|||||||
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
||||||
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true)
|
.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(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true),
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true),
|
||||||
new AttackMove(MoveId.SUPERPOWER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
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)
|
.attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false)
|
||||||
.triageMove(),
|
.triageMove(),
|
||||||
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
||||||
.ignoresProtect()
|
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
.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)
|
new StatusMove(MoveId.MIRACLE_EYE, PokemonType.PSYCHIC, -1, 40, -1, 0, 4)
|
||||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
@ -9745,7 +9771,8 @@ export function initMoves() {
|
|||||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||||
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
.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)
|
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
||||||
.attr(RecoilAttr, false, 0.33)
|
.attr(RecoilAttr, false, 0.33)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
@ -9974,12 +10001,12 @@ export function initMoves() {
|
|||||||
.powderMove()
|
.powderMove()
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||||
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
|
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.TELEKINESIS, false, true, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, 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(),
|
.reflectable(),
|
||||||
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
@ -9987,8 +10014,6 @@ export function initMoves() {
|
|||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
|
new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
|
||||||
.attr(FallDownAttr)
|
.attr(FallDownAttr)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
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(FallDownAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6)
|
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,
|
MysteryEncounterType.DARK_DEAL,
|
||||||
)
|
)
|
||||||
.withEncounterTier(MysteryEncounterTier.ROGUE)
|
.withEncounterTier(MysteryEncounterTier.ROGUE)
|
||||||
|
.withDisallowedChallenges(Challenges.HARDCORE)
|
||||||
.withIntroSpriteConfigs([
|
.withIntroSpriteConfigs([
|
||||||
{
|
{
|
||||||
spriteKey: "dark_deal_scientist",
|
spriteKey: "dark_deal_scientist",
|
||||||
|
@ -673,6 +673,8 @@ export async function catchPokemon(
|
|||||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
const addStatus = new BooleanHolder(true);
|
||||||
|
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||||
const doPokemonCatchMenu = () => {
|
const doPokemonCatchMenu = () => {
|
||||||
const end = () => {
|
const end = () => {
|
||||||
// Ensure the pokemon is in the enemy party in all situations
|
// 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(() => {
|
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||||
const addStatus = new BooleanHolder(true);
|
if (!(isObtain || addStatus.value)) {
|
||||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
|
||||||
if (!addStatus.value) {
|
|
||||||
removePokemon();
|
removePokemon();
|
||||||
end();
|
end();
|
||||||
return;
|
return;
|
||||||
@ -807,10 +807,16 @@ export async function catchPokemon(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (showCatchObtainMessage) {
|
if (showCatchObtainMessage) {
|
||||||
|
let catchMessage: string;
|
||||||
|
if (isObtain) {
|
||||||
|
catchMessage = "battle:pokemonObtained";
|
||||||
|
} else if (addStatus.value) {
|
||||||
|
catchMessage = "battle:pokemonCaught";
|
||||||
|
} else {
|
||||||
|
catchMessage = "battle:pokemonCaughtButChallenge";
|
||||||
|
}
|
||||||
globalScene.ui.showText(
|
globalScene.ui.showText(
|
||||||
i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", {
|
i18next.t(catchMessage, { pokemonName: pokemon.getNameToRender() }),
|
||||||
pokemonName: pokemon.getNameToRender(),
|
|
||||||
}),
|
|
||||||
null,
|
null,
|
||||||
doPokemonCatchMenu,
|
doPokemonCatchMenu,
|
||||||
0,
|
0,
|
||||||
|
@ -23,17 +23,21 @@ export class Terrain {
|
|||||||
public terrainType: TerrainType;
|
public terrainType: TerrainType;
|
||||||
public turnsLeft: number;
|
public turnsLeft: number;
|
||||||
|
|
||||||
constructor(terrainType: TerrainType, turnsLeft?: number) {
|
constructor(terrainType: TerrainType, turnsLeft = 0) {
|
||||||
this.terrainType = terrainType;
|
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 {
|
lapse(): boolean {
|
||||||
if (this.turnsLeft) {
|
// TODO: Add separate flag for infinite duration terrains
|
||||||
return !!--this.turnsLeft;
|
if (this.turnsLeft <= 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return --this.turnsLeft > 0;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttackTypeMultiplier(attackType: PokemonType): number {
|
getAttackTypeMultiplier(attackType: PokemonType): number {
|
||||||
|
@ -20,20 +20,25 @@ export class Weather {
|
|||||||
public weatherType: WeatherType;
|
public weatherType: WeatherType;
|
||||||
public turnsLeft: number;
|
public turnsLeft: number;
|
||||||
|
|
||||||
constructor(weatherType: WeatherType, turnsLeft?: number) {
|
constructor(weatherType: WeatherType, turnsLeft = 0) {
|
||||||
this.weatherType = weatherType;
|
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 {
|
lapse(): boolean {
|
||||||
if (this.isImmutable()) {
|
if (this.isImmutable()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.turnsLeft) {
|
|
||||||
return !!--this.turnsLeft;
|
if (this.turnsLeft <= 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return --this.turnsLeft > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
isImmutable(): boolean {
|
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 {
|
export function getWeatherStartMessage(weatherType: WeatherType): string | null {
|
||||||
switch (weatherType) {
|
switch (weatherType) {
|
||||||
case WeatherType.SUNNY:
|
case WeatherType.SUNNY:
|
||||||
|
@ -288,20 +288,18 @@ export class Arena {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets weather to the override specified in overrides.ts
|
* Sets weather to the override specified in overrides.ts`
|
||||||
* @param weather new {@linkcode WeatherType} to set
|
|
||||||
* @returns true to force trySetWeather to return true
|
|
||||||
*/
|
*/
|
||||||
trySetWeatherOverride(weather: WeatherType): boolean {
|
private overrideWeather(): void {
|
||||||
|
const weather = Overrides.WEATHER_OVERRIDE;
|
||||||
this.weather = new Weather(weather, 0);
|
this.weather = new Weather(weather, 0);
|
||||||
globalScene.phaseManager.unshiftNew("CommonAnimPhase", undefined, undefined, CommonAnim.SUNNY + (weather - 1));
|
globalScene.phaseManager.unshiftNew("CommonAnimPhase", undefined, undefined, CommonAnim.SUNNY + (weather - 1));
|
||||||
globalScene.phaseManager.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct?
|
globalScene.phaseManager.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct?
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns weather or not the weather can be changed to {@linkcode weather} */
|
/** Returns weather or not the weather can be changed to {@linkcode weather} */
|
||||||
canSetWeather(weather: WeatherType): boolean {
|
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 {
|
trySetWeather(weather: WeatherType, user?: Pokemon): boolean {
|
||||||
if (Overrides.WEATHER_OVERRIDE) {
|
if (Overrides.WEATHER_OVERRIDE) {
|
||||||
return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE);
|
this.overrideWeather();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canSetWeather(weather)) {
|
if (!this.canSetWeather(weather)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldWeatherType = this.weather?.weatherType || WeatherType.NONE;
|
const oldWeatherType = this.getWeatherType();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.weather?.isImmutable() &&
|
this.weather?.isImmutable() &&
|
||||||
@ -343,7 +342,7 @@ export class Arena {
|
|||||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
|
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(
|
this.eventTarget.dispatchEvent(
|
||||||
new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!),
|
new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!),
|
||||||
); // TODO: is this bang correct?
|
); // 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 {
|
canSetTerrain(terrain: TerrainType): boolean {
|
||||||
return !(this.terrain?.terrainType === (terrain || undefined));
|
return this.getTerrainType() !== terrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to set a new terrain effect to the battle
|
* Attempt to set the current terrain to the specified type.
|
||||||
* @param terrain {@linkcode TerrainType} new {@linkcode TerrainType} to set
|
* @param terrain - The {@linkcode TerrainType} to try and set.
|
||||||
* @param ignoreAnim boolean if the terrain animation should be ignored
|
* @param ignoreAnim - Whether to prevent showing an the animation; default `false`
|
||||||
* @param user {@linkcode Pokemon} that caused the terrain effect
|
* @param user - The {@linkcode Pokemon} creating the terrain (if any)
|
||||||
* @returns true if new terrain set, false if no terrain provided or attempting to set the same terrain as currently in use
|
* @returns Whether the terrain was successfully set.
|
||||||
*/
|
*/
|
||||||
trySetTerrain(terrain: TerrainType, ignoreAnim = false, user?: Pokemon): boolean {
|
trySetTerrain(terrain: TerrainType, ignoreAnim = false, user?: Pokemon): boolean {
|
||||||
if (!this.canSetTerrain(terrain)) {
|
if (!this.canSetTerrain(terrain)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldTerrainType = this.terrain?.terrainType || TerrainType.NONE;
|
const oldTerrainType = this.getTerrainType();
|
||||||
|
|
||||||
const terrainDuration = new NumberHolder(0);
|
const terrainDuration = new NumberHolder(0);
|
||||||
|
|
||||||
if (!isNullOrUndefined(user)) {
|
if (!isNullOrUndefined(user)) {
|
||||||
@ -430,7 +428,7 @@ export class Arena {
|
|||||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
|
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(
|
this.eventTarget.dispatchEvent(
|
||||||
new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!),
|
new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!),
|
||||||
@ -464,6 +462,24 @@ export class Arena {
|
|||||||
return true;
|
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 {
|
public isMoveWeatherCancelled(user: Pokemon, move: Move): boolean {
|
||||||
return !!this.weather && !this.weather.isEffectSuppressed() && this.weather.isMoveWeatherCancelled(user, move);
|
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;
|
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 (
|
return (
|
||||||
!!this.getTag(GroundedTag) ||
|
!!this.getTag(GroundedTag) ||
|
||||||
|
globalScene.arena.hasTag(ArenaTagType.GRAVITY) ||
|
||||||
(!this.isOfType(PokemonType.FLYING, true, true) &&
|
(!this.isOfType(PokemonType.FLYING, true, true) &&
|
||||||
!this.hasAbility(AbilityId.LEVITATE) &&
|
!this.hasAbility(AbilityId.LEVITATE) &&
|
||||||
!this.getTag(BattlerTagType.FLOATING) &&
|
!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
|
// 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
|
// 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);
|
const flyingIndex = types.indexOf(PokemonType.FLYING);
|
||||||
if (flyingIndex > -1) {
|
if (flyingIndex > -1) {
|
||||||
types.splice(flyingIndex, 1);
|
types.splice(flyingIndex, 1);
|
||||||
@ -3755,6 +3771,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||||
|
|
||||||
/** Combined damage multiplier from field effects such as weather, terrain, etc. */
|
/** Combined damage multiplier from field effects such as weather, terrain, etc. */
|
||||||
|
// TODO: This should be applied directly to base power
|
||||||
const arenaAttackTypeMultiplier = new NumberHolder(
|
const arenaAttackTypeMultiplier = new NumberHolder(
|
||||||
globalScene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()),
|
globalScene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { type PokeballCounts } from "#app/battle-scene";
|
import { type PokeballCounts } from "#app/battle-scene";
|
||||||
|
import { TerrainType } from "#app/data/terrain";
|
||||||
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -61,6 +62,12 @@ class DefaultOverrides {
|
|||||||
readonly SEED_OVERRIDE: string = "";
|
readonly SEED_OVERRIDE: string = "";
|
||||||
readonly DAILY_RUN_SEED_OVERRIDE: string | null = null;
|
readonly DAILY_RUN_SEED_OVERRIDE: string | null = null;
|
||||||
readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
|
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.
|
* If `null`, ignore this override.
|
||||||
*
|
*
|
||||||
|
@ -253,8 +253,11 @@ export class AttemptCapturePhase extends PokemonPhase {
|
|||||||
|
|
||||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
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(
|
globalScene.ui.showText(
|
||||||
i18next.t("battle:pokemonCaught", {
|
i18next.t(addStatus.value ? "battle:pokemonCaught" : "battle:pokemonCaughtButChallenge", {
|
||||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||||
}),
|
}),
|
||||||
null,
|
null,
|
||||||
@ -290,8 +293,6 @@ export class AttemptCapturePhase extends PokemonPhase {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
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 (!addStatus.value) {
|
||||||
removePokemon();
|
removePokemon();
|
||||||
end();
|
end();
|
||||||
|
@ -690,6 +690,7 @@ export class EncounterPhase extends BattlePhase {
|
|||||||
trySetWeatherIfNewBiome(): void {
|
trySetWeatherIfNewBiome(): void {
|
||||||
if (!this.loaded) {
|
if (!this.loaded) {
|
||||||
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
|
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
|
||||||
|
globalScene.arena.tryOverrideTerrain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,10 @@ export class SelectBiomePhase extends BattlePhase {
|
|||||||
|
|
||||||
globalScene.resetSeed();
|
globalScene.resetSeed();
|
||||||
|
|
||||||
|
const gameMode = globalScene.gameMode;
|
||||||
const currentBiome = globalScene.arena.biomeType;
|
const currentBiome = globalScene.arena.biomeType;
|
||||||
const nextWaveIndex = globalScene.currentBattle.waveIndex + 1;
|
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||||
|
const nextWaveIndex = currentWaveIndex + 1;
|
||||||
|
|
||||||
const setNextBiome = (nextBiome: BiomeId) => {
|
const setNextBiome = (nextBiome: BiomeId) => {
|
||||||
if (nextWaveIndex % 10 === 1) {
|
if (nextWaveIndex % 10 === 1) {
|
||||||
@ -26,6 +28,15 @@ export class SelectBiomePhase extends BattlePhase {
|
|||||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||||
if (healStatus.value) {
|
if (healStatus.value) {
|
||||||
globalScene.phaseManager.unshiftNew("PartyHealPhase", false);
|
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);
|
globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome);
|
||||||
@ -33,12 +44,12 @@ export class SelectBiomePhase extends BattlePhase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
(gameMode.isClassic && gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) ||
|
(gameMode.isDaily && gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||||
(globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
(gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||||
) {
|
) {
|
||||||
setNextBiome(BiomeId.END);
|
setNextBiome(BiomeId.END);
|
||||||
} else if (globalScene.gameMode.hasRandomBiomes) {
|
} else if (gameMode.hasRandomBiomes) {
|
||||||
setNextBiome(this.generateNextBiome(nextWaveIndex));
|
setNextBiome(this.generateNextBiome(nextWaveIndex));
|
||||||
} else if (Array.isArray(biomeLinks[currentBiome])) {
|
} else if (Array.isArray(biomeLinks[currentBiome])) {
|
||||||
const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[])
|
const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[])
|
||||||
@ -73,9 +84,6 @@ export class SelectBiomePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateNextBiome(waveIndex: number): BiomeId {
|
generateNextBiome(waveIndex: number): BiomeId {
|
||||||
if (!(waveIndex % 50)) {
|
return waveIndex % 50 === 0 ? BiomeId.END : globalScene.generateRandomBiome(waveIndex);
|
||||||
return BiomeId.END;
|
|
||||||
}
|
|
||||||
return globalScene.generateRandomBiome(waveIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,9 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { modifierTypes } from "#data/data-lists";
|
import { modifierTypes } from "#data/data-lists";
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
import { ChallengeType } from "#enums/challenge-type";
|
|
||||||
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
||||||
import type { CustomModifierSettings } from "#modifiers/modifier-type";
|
|
||||||
import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils";
|
import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils";
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
|
||||||
import { BooleanHolder } from "#utils/common";
|
|
||||||
|
|
||||||
export class VictoryPhase extends PokemonPhase {
|
export class VictoryPhase extends PokemonPhase {
|
||||||
public readonly phaseName = "VictoryPhase";
|
public readonly phaseName = "VictoryPhase";
|
||||||
@ -49,15 +45,19 @@ export class VictoryPhase extends PokemonPhase {
|
|||||||
if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
|
if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
|
||||||
globalScene.phaseManager.pushNew("TrainerVictoryPhase");
|
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");
|
globalScene.phaseManager.pushNew("EggLapsePhase");
|
||||||
if (globalScene.gameMode.isClassic) {
|
if (gameMode.isClassic) {
|
||||||
switch (globalScene.currentBattle.waveIndex) {
|
switch (currentWaveIndex) {
|
||||||
case ClassicFixedBossWaves.RIVAL_1:
|
case ClassicFixedBossWaves.RIVAL_1:
|
||||||
case ClassicFixedBossWaves.RIVAL_2:
|
case ClassicFixedBossWaves.RIVAL_2:
|
||||||
// Get event modifiers for this wave
|
// Get event modifiers for this wave
|
||||||
timedEventManager
|
timedEventManager
|
||||||
.getFixedBattleEventRewards(globalScene.currentBattle.waveIndex)
|
.getFixedBattleEventRewards(currentWaveIndex)
|
||||||
.map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r]));
|
.map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r]));
|
||||||
break;
|
break;
|
||||||
case ClassicFixedBossWaves.EVIL_BOSS_2:
|
case ClassicFixedBossWaves.EVIL_BOSS_2:
|
||||||
@ -66,59 +66,53 @@ export class VictoryPhase extends PokemonPhase {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0);
|
if (currentWaveIndex % 10) {
|
||||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
|
||||||
if (!healStatus.value) {
|
|
||||||
globalScene.phaseManager.pushNew(
|
globalScene.phaseManager.pushNew(
|
||||||
"SelectModifierPhase",
|
"SelectModifierPhase",
|
||||||
undefined,
|
undefined,
|
||||||
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);
|
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM);
|
||||||
if (
|
if (currentWaveIndex > 10 && !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||||
globalScene.currentBattle.waveIndex > 10 &&
|
|
||||||
!globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)
|
|
||||||
) {
|
|
||||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
const superExpWave = !gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||||
if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) {
|
if (gameMode.isEndless && currentWaveIndex === 10) {
|
||||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE);
|
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE);
|
||||||
}
|
}
|
||||||
if (
|
if (currentWaveIndex <= 750 && (currentWaveIndex <= 500 || currentWaveIndex % 30 === superExpWave)) {
|
||||||
globalScene.currentBattle.waveIndex <= 750 &&
|
|
||||||
(globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave)
|
|
||||||
) {
|
|
||||||
globalScene.phaseManager.pushNew(
|
globalScene.phaseManager.pushNew(
|
||||||
"ModifierRewardPhase",
|
"ModifierRewardPhase",
|
||||||
globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250
|
currentWaveIndex % 30 !== superExpWave || currentWaveIndex > 250
|
||||||
? modifierTypes.EXP_CHARM
|
? modifierTypes.EXP_CHARM
|
||||||
: modifierTypes.SUPER_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);
|
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||||
}
|
}
|
||||||
if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) {
|
if (gameMode.isEndless && !(currentWaveIndex % 50)) {
|
||||||
globalScene.phaseManager.pushNew(
|
globalScene.phaseManager.pushNew(
|
||||||
"ModifierRewardPhase",
|
"ModifierRewardPhase",
|
||||||
!(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
!(currentWaveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||||
);
|
);
|
||||||
globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase");
|
globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
if (gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||||
globalScene.phaseManager.pushNew("SelectBiomePhase");
|
globalScene.phaseManager.pushNew("SelectBiomePhase");
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.pushNew("NewBattlePhase");
|
globalScene.phaseManager.pushNew("NewBattlePhase");
|
||||||
} else {
|
} else {
|
||||||
globalScene.currentBattle.battleType = BattleType.CLEAR;
|
globalScene.currentBattle.battleType = BattleType.CLEAR;
|
||||||
globalScene.score += globalScene.gameMode.getClearScoreBonus();
|
globalScene.score += gameMode.getClearScoreBonus();
|
||||||
globalScene.updateScoreText();
|
globalScene.updateScoreText();
|
||||||
globalScene.phaseManager.pushNew("GameOverPhase", true);
|
globalScene.phaseManager.pushNew("GameOverPhase", true);
|
||||||
}
|
}
|
||||||
@ -126,18 +120,4 @@ export class VictoryPhase extends PokemonPhase {
|
|||||||
|
|
||||||
this.end();
|
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 {
|
export class WeatherEffectPhase extends CommonAnimPhase {
|
||||||
public readonly phaseName = "WeatherEffectPhase";
|
public readonly phaseName = "WeatherEffectPhase";
|
||||||
public weather: Weather | null;
|
public weather: Weather | null; // TODO: This should not be `null`
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
|
@ -448,6 +448,8 @@ export function getAchievementDescription(localizationKey: string): string {
|
|||||||
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
|
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
|
||||||
case "FLIP_INVERSE":
|
case "FLIP_INVERSE":
|
||||||
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
|
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
|
||||||
|
case "NUZLOCKE":
|
||||||
|
return i18next.t("achv:NUZLOCKE.description", { context: genderStr });
|
||||||
case "BREEDERS_IN_SPACE":
|
case "BREEDERS_IN_SPACE":
|
||||||
return i18next.t("achv:BREEDERS_IN_SPACE.description", {
|
return i18next.t("achv:BREEDERS_IN_SPACE.description", {
|
||||||
context: genderStr,
|
context: genderStr,
|
||||||
|
@ -208,6 +208,26 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
|||||||
);
|
);
|
||||||
this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant);
|
this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant);
|
||||||
this.add(this.icon);
|
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) {
|
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:hasHiddenAbility"), undefined, DropDownState.ON),
|
||||||
new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE),
|
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 = [
|
const eggLabels = [
|
||||||
new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF),
|
new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF),
|
||||||
new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON),
|
new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON),
|
||||||
@ -423,6 +428,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
|||||||
new DropDownOption("FAVORITE", favoriteLabels),
|
new DropDownOption("FAVORITE", favoriteLabels),
|
||||||
new DropDownOption("WIN", winLabels),
|
new DropDownOption("WIN", winLabels),
|
||||||
new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels),
|
new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels),
|
||||||
|
new DropDownOption("SEEN_SPECIES", seenSpeciesLabels),
|
||||||
new DropDownOption("EGG", eggLabels),
|
new DropDownOption("EGG", eggLabels),
|
||||||
new DropDownOption("POKERUS", pokerusLabels),
|
new DropDownOption("POKERUS", pokerusLabels),
|
||||||
];
|
];
|
||||||
@ -792,13 +798,15 @@ export class PokedexUiHandler extends MessageUiHandler {
|
|||||||
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
|
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean {
|
isSeen(species: PokemonSpecies, dexEntry: DexEntry, seenFilter?: boolean): boolean {
|
||||||
if (dexEntry?.seenAttr) {
|
if (dexEntry?.seenAttr) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!seenFilter) {
|
||||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||||
return !!starterDexEntry?.caughtAttr;
|
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
|
// Egg Purchasable Filter
|
||||||
const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId);
|
const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId);
|
||||||
const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||||
@ -1658,6 +1681,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
|||||||
fitsFavorite &&
|
fitsFavorite &&
|
||||||
fitsWin &&
|
fitsWin &&
|
||||||
fitsHA &&
|
fitsHA &&
|
||||||
|
fitsSeen &&
|
||||||
fitsEgg &&
|
fitsEgg &&
|
||||||
fitsPokerus
|
fitsPokerus
|
||||||
) {
|
) {
|
||||||
|
@ -210,7 +210,8 @@ export class RunInfoUiHandler extends UiHandler {
|
|||||||
this.runContainer.add(headerText);
|
this.runContainer.add(headerText);
|
||||||
const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW);
|
const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW);
|
||||||
runName.setOrigin(0, 0);
|
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);
|
this.runContainer.add(runName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,27 +278,6 @@ describe("Abilities - Parental Bond", () => {
|
|||||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
|
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 () => {
|
it("U-turn boosted by this ability should strike twice before forcing a switch", async () => {
|
||||||
game.override.moveset([MoveId.U_TURN]);
|
game.override.moveset([MoveId.U_TURN]);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Status } from "#data/status-effect";
|
import { Status } from "#data/status-effect";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
@ -104,27 +105,26 @@ describe("Abilities - SHIELDS DOWN", () => {
|
|||||||
expect(game.field.getPlayerPokemon().status).toBe(undefined);
|
expect(game.field.getPlayerPokemon().status).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// toxic spikes currently does not poison flying types when gravity is in effect
|
it("should be poisoned by toxic spikes when Gravity is active before changing forms", async () => {
|
||||||
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]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]);
|
||||||
|
|
||||||
// turn 1
|
// Change minior to core form in a state where it would revert on switch
|
||||||
game.move.select(MoveId.GRAVITY);
|
const minior = game.scene.getPlayerParty()[1];
|
||||||
await game.move.selectEnemyMove(MoveId.TOXIC_SPIKES);
|
minior.formIndex = redCoreForm;
|
||||||
|
|
||||||
|
game.move.use(MoveId.GRAVITY);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TOXIC_SPIKES);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
// turn 2
|
expect(game).toHaveArenaTag(ArenaTagType.GRAVITY);
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
game.doSwitchPokemon(1);
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.MINIOR);
|
expect(minior.species.speciesId).toBe(SpeciesId.MINIOR);
|
||||||
expect(game.field.getPlayerPokemon().species.formIndex).toBe(0);
|
expect(minior.formIndex).toBe(0);
|
||||||
expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.POISON);
|
expect(minior.isGrounded()).toBe(true);
|
||||||
|
expect(minior).toHaveStatusEffect(StatusEffect.POISON);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should ignore yawn", async () => {
|
test("should ignore yawn", async () => {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
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 { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
@ -27,130 +25,49 @@ describe("Arena - Gravity", () => {
|
|||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.moveset([MoveId.TACKLE, MoveId.GRAVITY, MoveId.FISSURE])
|
|
||||||
.ability(AbilityId.UNNERVE)
|
.ability(AbilityId.UNNERVE)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.enemySpecies(SpeciesId.SHUCKLE)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
|
||||||
.enemyLevel(5);
|
.enemyLevel(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
||||||
|
|
||||||
it("non-OHKO move accuracy is multiplied by 1.67", async () => {
|
it("should multiply all non-OHKO move accuracy by 1.67x", async () => {
|
||||||
const moveToCheck = allMoves[MoveId.TACKLE];
|
const accSpy = vi.spyOn(allMoves[MoveId.TACKLE], "calculateBattleAccuracy");
|
||||||
|
|
||||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
|
||||||
|
|
||||||
// Setup Gravity on first turn
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
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();
|
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||||
|
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67);
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("OHKO move accuracy is not affected", async () => {
|
it("should not affect OHKO move accuracy", async () => {
|
||||||
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
|
const accSpy = vi.spyOn(allMoves[MoveId.FISSURE], "calculateBattleAccuracy");
|
||||||
const moveToCheck = allMoves[MoveId.FISSURE];
|
|
||||||
|
|
||||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
|
||||||
|
|
||||||
// Setup Gravity on first turn
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
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();
|
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||||
|
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.FISSURE].accuracy);
|
||||||
// Use OHKO move on second turn
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.FISSURE);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Against flying types", () => {
|
it("should forcibly ground all Pokemon for the duration of the effect", async () => {
|
||||||
it("can be hit by ground-type moves now", async () => {
|
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.EARTHQUAKE]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
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);
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.GRAVITY);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
|
|
||||||
|
|
||||||
game.move.select(MoveId.GRAVITY);
|
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
expect(player.isGrounded()).toBe(true);
|
||||||
|
expect(enemy.isGrounded()).toBe(true);
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 { AbilityId } from "#enums/ability-id";
|
||||||
import { Challenges } from "#enums/challenges";
|
import { Challenges } from "#enums/challenges";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
import { PokeballType } from "#enums/pokeball";
|
import { PokeballType } from "#enums/pokeball";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
@ -52,4 +54,18 @@ describe("Challenges - Limited Catch", () => {
|
|||||||
|
|
||||||
expect(game.scene.getPlayerParty()).toHaveLength(1);
|
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 { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { UiMode } from "#enums/ui-mode";
|
import { UiMode } from "#enums/ui-mode";
|
||||||
|
import { ExpBoosterModifier } from "#modifiers/modifier";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
@ -75,6 +76,7 @@ describe("Challenges - Limited Support", () => {
|
|||||||
await game.doKillOpponents();
|
await game.doKillOpponents();
|
||||||
await game.toNextWave();
|
await game.toNextWave();
|
||||||
|
|
||||||
|
expect(game.scene.getModifiers(ExpBoosterModifier)).toHaveLength(1);
|
||||||
expect(playerPokemon).not.toHaveFullHp();
|
expect(playerPokemon).not.toHaveFullHp();
|
||||||
|
|
||||||
game.move.use(MoveId.SPLASH);
|
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 { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
@ -28,30 +29,16 @@ describe("Moves - Magnet Rise", () => {
|
|||||||
.enemyLevel(1);
|
.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]);
|
await game.classicMode.startBattle([SpeciesId.MAGNEZONE]);
|
||||||
|
|
||||||
game.move.use(MoveId.MAGNET_RISE);
|
game.move.use(MoveId.MAGNET_RISE);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// magnezone levitated and was not hit by earthquake
|
||||||
const magnezone = game.field.getPlayerPokemon();
|
const magnezone = game.field.getPlayerPokemon();
|
||||||
expect(magnezone.hp).toBe(magnezone.getMaxHp());
|
expect(magnezone.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||||
expect(magnezone.isGrounded()).toBe(false);
|
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());
|
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 { AbilityId } from "#enums/ability-id";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { HitCheckResult } from "#enums/hit-check-result";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
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 { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@ -26,114 +29,117 @@ describe("Moves - Telekinesis", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.TELEKINESIS, MoveId.TACKLE, MoveId.MUD_SHOT, MoveId.SMACK_DOWN])
|
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemySpecies(SpeciesId.SNORLAX)
|
||||||
.enemyLevel(60)
|
.enemyLevel(60)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.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]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const player = game.field.getPlayerPokemon();
|
||||||
|
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();
|
await game.toNextTurn();
|
||||||
vi.spyOn(allMoves[MoveId.TACKLE], "accuracy", "get").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.TACKLE);
|
expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
expect(enemy).toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||||
expect(enemyOpponent.isFullHp()).toBe(false);
|
|
||||||
|
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]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.toNextTurn();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
// 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();
|
await game.toNextTurn();
|
||||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(100);
|
|
||||||
game.move.select(MoveId.MUD_SHOT);
|
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
expect(hitSpy).toHaveLastReturnedWith([HitCheckResult.NO_EFFECT, 0]);
|
||||||
expect(enemyOpponent.isFullHp()).toBe(true);
|
|
||||||
|
// 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 () => {
|
// TODO: Make an it.each testing the invalid species for Telekinesis
|
||||||
game.override.enemyMoveset(MoveId.TRANSFORM);
|
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]);
|
await game.classicMode.startBattle([SpeciesId.DIGLETT]);
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const enemyOpponent = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
await game.toNextTurn();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
|
||||||
|
expect(enemyOpponent).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
|
expect(enemyOpponent).toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||||
expect(enemyOpponent.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT);
|
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 () => {
|
it("should become grounded when Ingrain is used, but not remove the guaranteed hit effect", 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]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const enemy = 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();
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.MUD_SHOT);
|
game.move.use(MoveId.MUD_SHOT);
|
||||||
await game.move.selectEnemyMove(MoveId.INGRAIN);
|
await game.move.forceEnemyMove(MoveId.INGRAIN);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.INGRAIN)).toBeDefined();
|
await game.move.forceMiss();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
await game.toEndOfTurn();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
|
||||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
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 () => {
|
it("should not be baton passable onto a mega gengar", async () => {
|
||||||
game.override
|
game.override.starterForms({ [SpeciesId.GENGAR]: 1 });
|
||||||
.moveset([MoveId.BATON_PASS])
|
|
||||||
.enemyMoveset([MoveId.TELEKINESIS])
|
|
||||||
.starterForms({ [SpeciesId.GENGAR]: 1 });
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]);
|
||||||
game.move.select(MoveId.BATON_PASS);
|
|
||||||
|
game.move.use(MoveId.BATON_PASS);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TELEKINESIS);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
expect(game.field.getPlayerPokemon().getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
|
||||||
|
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 */
|
/** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
import type { NewArenaEvent } from "#events/battle-scene";
|
import type { NewArenaEvent } from "#events/battle-scene";
|
||||||
|
|
||||||
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
|
||||||
|
import { TerrainType } from "#app/data/terrain";
|
||||||
import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
|
import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -359,6 +361,19 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
return this;
|
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
|
* Override the seed
|
||||||
* @param seed - The seed to set
|
* @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 { NextEncounterPhase } from "#phases/next-encounter-phase";
|
||||||
import { PartyExpPhase } from "#phases/party-exp-phase";
|
import { PartyExpPhase } from "#phases/party-exp-phase";
|
||||||
import { PartyHealPhase } from "#phases/party-heal-phase";
|
import { PartyHealPhase } from "#phases/party-heal-phase";
|
||||||
|
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||||
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
||||||
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
||||||
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
||||||
@ -146,6 +147,7 @@ export class PhaseInterceptor {
|
|||||||
[PositionalTagPhase, this.startPhase],
|
[PositionalTagPhase, this.startPhase],
|
||||||
[PokemonTransformPhase, this.startPhase],
|
[PokemonTransformPhase, this.startPhase],
|
||||||
[MysteryEncounterPhase, this.startPhase],
|
[MysteryEncounterPhase, this.startPhase],
|
||||||
|
[PokemonHealPhase, this.startPhase],
|
||||||
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
||||||
[MysteryEncounterBattlePhase, this.startPhase],
|
[MysteryEncounterBattlePhase, this.startPhase],
|
||||||
[MysteryEncounterRewardsPhase, this.startPhase],
|
[MysteryEncounterRewardsPhase, this.startPhase],
|
||||||
|
Loading…
Reference in New Issue
Block a user