mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 22:09:27 +02:00
Compare commits
14 Commits
c1dfb4e394
...
177b11877b
Author | SHA1 | Date | |
---|---|---|---|
|
177b11877b | ||
|
46c78a0540 | ||
|
98809c28bd | ||
|
e0559e03ff | ||
|
f6b99780fb | ||
|
19af9bdb8b | ||
|
8e61b642a3 | ||
|
7e402d02b0 | ||
|
371e99a4a2 | ||
|
13a4b99072 | ||
|
216018b409 | ||
|
7c60d0a5b1 | ||
|
f5e0ddd7af | ||
|
1f50ebdae0 |
@ -1 +1 @@
|
||||
Subproject commit ab2716d5440c25f73986664aa3f3131821c3c392
|
||||
Subproject commit 1ea8f865e30d1940caa0fceeabf37ae2e4689471
|
@ -727,9 +727,7 @@ export class AttackTypeImmunityAbAttr extends TypeImmunityAbAttr {
|
||||
override canApply(params: TypeMultiplierAbAttrParams): boolean {
|
||||
const { move } = params;
|
||||
return (
|
||||
move.category !== MoveCategory.STATUS &&
|
||||
!move.hasAttr("NeutralDamageAgainstFlyingTypeMultiplierAttr") &&
|
||||
super.canApply(params)
|
||||
move.category !== MoveCategory.STATUS && !move.hasAttr("VariableMoveTypeChartAttr") && super.canApply(params)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4188,71 +4186,43 @@ function getWeatherCondition(...weatherTypes: WeatherType[]): AbAttrCondition {
|
||||
if (globalScene.arena.weather?.isEffectSuppressed()) {
|
||||
return false;
|
||||
}
|
||||
const weatherType = globalScene.arena.weather?.weatherType;
|
||||
return !!weatherType && weatherTypes.indexOf(weatherType) > -1;
|
||||
return weatherTypes.includes(globalScene.arena.getWeatherType());
|
||||
};
|
||||
}
|
||||
|
||||
function getAnticipationCondition(): AbAttrCondition {
|
||||
return (pokemon: Pokemon) => {
|
||||
for (const opponent of pokemon.getOpponents()) {
|
||||
for (const move of opponent.moveset) {
|
||||
// ignore null/undefined moves
|
||||
if (!move) {
|
||||
continue;
|
||||
}
|
||||
// the move's base type (not accounting for variable type changes) is super effective
|
||||
if (
|
||||
move.getMove().is("AttackMove") &&
|
||||
pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// move is a OHKO
|
||||
if (move.getMove().hasAttr("OneHitKOAttr")) {
|
||||
return true;
|
||||
}
|
||||
// edge case for hidden power, type is computed
|
||||
if (move.getMove().id === MoveId.HIDDEN_POWER) {
|
||||
const iv_val = Math.floor(
|
||||
(((opponent.ivs[Stat.HP] & 1) +
|
||||
(opponent.ivs[Stat.ATK] & 1) * 2 +
|
||||
(opponent.ivs[Stat.DEF] & 1) * 4 +
|
||||
(opponent.ivs[Stat.SPD] & 1) * 8 +
|
||||
(opponent.ivs[Stat.SPATK] & 1) * 16 +
|
||||
(opponent.ivs[Stat.SPDEF] & 1) * 32) *
|
||||
15) /
|
||||
63,
|
||||
);
|
||||
|
||||
const type = [
|
||||
PokemonType.FIGHTING,
|
||||
PokemonType.FLYING,
|
||||
PokemonType.POISON,
|
||||
PokemonType.GROUND,
|
||||
PokemonType.ROCK,
|
||||
PokemonType.BUG,
|
||||
PokemonType.GHOST,
|
||||
PokemonType.STEEL,
|
||||
PokemonType.FIRE,
|
||||
PokemonType.WATER,
|
||||
PokemonType.GRASS,
|
||||
PokemonType.ELECTRIC,
|
||||
PokemonType.PSYCHIC,
|
||||
PokemonType.ICE,
|
||||
PokemonType.DRAGON,
|
||||
PokemonType.DARK,
|
||||
][iv_val];
|
||||
|
||||
if (pokemon.getAttackTypeEffectiveness(type, opponent) >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Condition used by {@linkcode AbilityId.ANTICIPATION} to show a message if any opponent knows a
|
||||
* "dangerous" move.
|
||||
* @param pokemon - The {@linkcode Pokemon} with this ability
|
||||
* @returns Whether the message should be shown
|
||||
*/
|
||||
const anticipationCondition: AbAttrCondition = (pokemon: Pokemon) =>
|
||||
pokemon.getOpponents().some(opponent =>
|
||||
opponent.moveset.some(movesetMove => {
|
||||
// ignore null/undefined moves or non-attacks
|
||||
const move = movesetMove?.getMove();
|
||||
if (!move?.is("AttackMove")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
if (move.hasAttr("OneHitKOAttr")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check whether the move's base type (not accounting for variable type changes) is super effective
|
||||
const type = new NumberHolder(
|
||||
pokemon.getAttackTypeEffectiveness(move.type, {
|
||||
source: opponent,
|
||||
ignoreStrongWinds: true,
|
||||
move: move,
|
||||
}),
|
||||
);
|
||||
|
||||
// edge case for hidden power, type is computed
|
||||
applyMoveAttrs("HiddenPowerTypeAttr", opponent, pokemon, move, type);
|
||||
return type.value >= 2;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates an ability condition that causes the ability to fail if that ability
|
||||
@ -7083,7 +7053,7 @@ export function initAbilities() {
|
||||
.attr(PostFaintContactDamageAbAttr, 4)
|
||||
.bypassFaint(),
|
||||
new Ability(AbilityId.ANTICIPATION, 4)
|
||||
.conditionalAttr(getAnticipationCondition(), PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAnticipation", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })),
|
||||
.conditionalAttr(anticipationCondition, PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAnticipation", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })),
|
||||
new Ability(AbilityId.FOREWARN, 4)
|
||||
.attr(ForewarnAbAttr),
|
||||
new Ability(AbilityId.UNAWARE, 4)
|
||||
|
@ -958,7 +958,7 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
getDamageHpRatio(pokemon: Pokemon): number {
|
||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, { ignoreStrongWinds: true });
|
||||
|
||||
let damageHpRatio = 0;
|
||||
|
||||
|
@ -45736,6 +45736,285 @@ export const tmSpecies: TmSpecies = {
|
||||
SpeciesId.HISUI_ARCANINE,
|
||||
SpeciesId.HISUI_AVALUGG,
|
||||
],
|
||||
[MoveId.SHOCK_WAVE]: [
|
||||
SpeciesId.RATTATA,
|
||||
SpeciesId.RATICATE,
|
||||
SpeciesId.PIKACHU,
|
||||
SpeciesId.RAICHU,
|
||||
SpeciesId.NIDORAN_F,
|
||||
SpeciesId.NIDORINA,
|
||||
SpeciesId.NIDOQUEEN,
|
||||
SpeciesId.NIDORAN_M,
|
||||
SpeciesId.NIDORINO,
|
||||
SpeciesId.NIDOKING,
|
||||
SpeciesId.CLEFAIRY,
|
||||
SpeciesId.CLEFABLE,
|
||||
SpeciesId.JIGGLYPUFF,
|
||||
SpeciesId.WIGGLYTUFF,
|
||||
SpeciesId.MEOWTH,
|
||||
SpeciesId.PERSIAN,
|
||||
SpeciesId.ABRA,
|
||||
SpeciesId.KADABRA,
|
||||
SpeciesId.ALAKAZAM,
|
||||
SpeciesId.MAGNEMITE,
|
||||
SpeciesId.MAGNETON,
|
||||
SpeciesId.GRIMER,
|
||||
SpeciesId.MUK,
|
||||
SpeciesId.VOLTORB,
|
||||
SpeciesId.ELECTRODE,
|
||||
SpeciesId.LICKITUNG,
|
||||
SpeciesId.KOFFING,
|
||||
SpeciesId.WEEZING,
|
||||
SpeciesId.RHYHORN,
|
||||
SpeciesId.RHYDON,
|
||||
SpeciesId.CHANSEY,
|
||||
SpeciesId.TANGELA,
|
||||
SpeciesId.KANGASKHAN,
|
||||
SpeciesId.MR_MIME,
|
||||
SpeciesId.ELECTABUZZ,
|
||||
SpeciesId.TAUROS,
|
||||
SpeciesId.LAPRAS,
|
||||
SpeciesId.JOLTEON,
|
||||
SpeciesId.PORYGON,
|
||||
SpeciesId.SNORLAX,
|
||||
SpeciesId.ZAPDOS,
|
||||
SpeciesId.DRATINI,
|
||||
SpeciesId.DRAGONAIR,
|
||||
SpeciesId.DRAGONITE,
|
||||
SpeciesId.MEWTWO,
|
||||
SpeciesId.MEW,
|
||||
SpeciesId.SENTRET,
|
||||
SpeciesId.FURRET,
|
||||
SpeciesId.CHINCHOU,
|
||||
SpeciesId.LANTURN,
|
||||
SpeciesId.PICHU,
|
||||
SpeciesId.CLEFFA,
|
||||
SpeciesId.IGGLYBUFF,
|
||||
SpeciesId.TOGEPI,
|
||||
SpeciesId.TOGETIC,
|
||||
SpeciesId.MAREEP,
|
||||
SpeciesId.FLAAFFY,
|
||||
SpeciesId.AMPHAROS,
|
||||
SpeciesId.AIPOM,
|
||||
SpeciesId.MISDREAVUS,
|
||||
SpeciesId.GIRAFARIG,
|
||||
SpeciesId.DUNSPARCE,
|
||||
SpeciesId.SNUBBULL,
|
||||
SpeciesId.GRANBULL,
|
||||
SpeciesId.QWILFISH,
|
||||
SpeciesId.PORYGON2,
|
||||
SpeciesId.STANTLER,
|
||||
SpeciesId.ELEKID,
|
||||
SpeciesId.MILTANK,
|
||||
SpeciesId.BLISSEY,
|
||||
SpeciesId.RAIKOU,
|
||||
SpeciesId.TYRANITAR,
|
||||
SpeciesId.LUGIA,
|
||||
SpeciesId.HO_OH,
|
||||
SpeciesId.CELEBI,
|
||||
SpeciesId.ZIGZAGOON,
|
||||
SpeciesId.LINOONE,
|
||||
SpeciesId.WINGULL,
|
||||
SpeciesId.PELIPPER,
|
||||
SpeciesId.RALTS,
|
||||
SpeciesId.KIRLIA,
|
||||
SpeciesId.GARDEVOIR,
|
||||
SpeciesId.SLAKOTH,
|
||||
SpeciesId.VIGOROTH,
|
||||
SpeciesId.SLAKING,
|
||||
SpeciesId.WHISMUR,
|
||||
SpeciesId.LOUDRED,
|
||||
SpeciesId.EXPLOUD,
|
||||
SpeciesId.NOSEPASS,
|
||||
SpeciesId.SKITTY,
|
||||
SpeciesId.DELCATTY,
|
||||
SpeciesId.SABLEYE,
|
||||
SpeciesId.ARON,
|
||||
SpeciesId.LAIRON,
|
||||
SpeciesId.AGGRON,
|
||||
SpeciesId.ELECTRIKE,
|
||||
SpeciesId.MANECTRIC,
|
||||
SpeciesId.PLUSLE,
|
||||
SpeciesId.MINUN,
|
||||
SpeciesId.VOLBEAT,
|
||||
SpeciesId.ILLUMISE,
|
||||
SpeciesId.GULPIN,
|
||||
SpeciesId.SWALOT,
|
||||
SpeciesId.SPOINK,
|
||||
SpeciesId.GRUMPIG,
|
||||
SpeciesId.SPINDA,
|
||||
SpeciesId.ZANGOOSE,
|
||||
SpeciesId.CASTFORM,
|
||||
SpeciesId.KECLEON,
|
||||
SpeciesId.SHUPPET,
|
||||
SpeciesId.BANETTE,
|
||||
SpeciesId.CHIMECHO,
|
||||
SpeciesId.ABSOL,
|
||||
SpeciesId.REGIROCK,
|
||||
SpeciesId.REGICE,
|
||||
SpeciesId.REGISTEEL,
|
||||
SpeciesId.LATIAS,
|
||||
SpeciesId.LATIOS,
|
||||
SpeciesId.KYOGRE,
|
||||
SpeciesId.GROUDON,
|
||||
SpeciesId.RAYQUAZA,
|
||||
SpeciesId.JIRACHI,
|
||||
SpeciesId.DEOXYS,
|
||||
SpeciesId.BIDOOF,
|
||||
SpeciesId.BIBAREL,
|
||||
SpeciesId.SHINX,
|
||||
SpeciesId.LUXIO,
|
||||
SpeciesId.LUXRAY,
|
||||
SpeciesId.CRANIDOS,
|
||||
SpeciesId.RAMPARDOS,
|
||||
SpeciesId.SHIELDON,
|
||||
SpeciesId.BASTIODON,
|
||||
SpeciesId.PACHIRISU,
|
||||
SpeciesId.AMBIPOM,
|
||||
SpeciesId.DRIFLOON,
|
||||
SpeciesId.DRIFBLIM,
|
||||
SpeciesId.BUNEARY,
|
||||
SpeciesId.LOPUNNY,
|
||||
SpeciesId.MISMAGIUS,
|
||||
SpeciesId.GLAMEOW,
|
||||
SpeciesId.PURUGLY,
|
||||
SpeciesId.CHINGLING,
|
||||
SpeciesId.MIME_JR,
|
||||
SpeciesId.HAPPINY,
|
||||
SpeciesId.SPIRITOMB,
|
||||
SpeciesId.MUNCHLAX,
|
||||
SpeciesId.MAGNEZONE,
|
||||
SpeciesId.LICKILICKY,
|
||||
SpeciesId.RHYPERIOR,
|
||||
SpeciesId.TANGROWTH,
|
||||
SpeciesId.ELECTIVIRE,
|
||||
SpeciesId.TOGEKISS,
|
||||
SpeciesId.PORYGON_Z,
|
||||
SpeciesId.GALLADE,
|
||||
SpeciesId.PROBOPASS,
|
||||
SpeciesId.FROSLASS,
|
||||
SpeciesId.ROTOM,
|
||||
SpeciesId.UXIE,
|
||||
SpeciesId.MESPRIT,
|
||||
SpeciesId.AZELF,
|
||||
SpeciesId.DIALGA,
|
||||
SpeciesId.PALKIA,
|
||||
SpeciesId.REGIGIGAS,
|
||||
SpeciesId.GIRATINA,
|
||||
SpeciesId.DARKRAI,
|
||||
SpeciesId.ARCEUS,
|
||||
SpeciesId.VICTINI,
|
||||
SpeciesId.PATRAT,
|
||||
SpeciesId.WATCHOG,
|
||||
SpeciesId.LILLIPUP,
|
||||
SpeciesId.HERDIER,
|
||||
SpeciesId.STOUTLAND,
|
||||
SpeciesId.MUNNA,
|
||||
SpeciesId.MUSHARNA,
|
||||
SpeciesId.BLITZLE,
|
||||
SpeciesId.ZEBSTRIKA,
|
||||
SpeciesId.WOOBAT,
|
||||
SpeciesId.SWOOBAT,
|
||||
SpeciesId.SIGILYPH,
|
||||
SpeciesId.YAMASK,
|
||||
SpeciesId.COFAGRIGUS,
|
||||
SpeciesId.MINCCINO,
|
||||
SpeciesId.CINCCINO,
|
||||
SpeciesId.GOTHITA,
|
||||
SpeciesId.GOTHORITA,
|
||||
SpeciesId.GOTHITELLE,
|
||||
SpeciesId.SOLOSIS,
|
||||
SpeciesId.DUOSION,
|
||||
SpeciesId.REUNICLUS,
|
||||
SpeciesId.EMOLGA,
|
||||
SpeciesId.FRILLISH,
|
||||
SpeciesId.JELLICENT,
|
||||
SpeciesId.JOLTIK,
|
||||
SpeciesId.GALVANTULA,
|
||||
SpeciesId.KLINK,
|
||||
SpeciesId.KLANG,
|
||||
SpeciesId.KLINKLANG,
|
||||
SpeciesId.EELEKTRIK,
|
||||
SpeciesId.EELEKTROSS,
|
||||
SpeciesId.ELGYEM,
|
||||
SpeciesId.BEHEEYEM,
|
||||
SpeciesId.LITWICK,
|
||||
SpeciesId.LAMPENT,
|
||||
SpeciesId.CHANDELURE,
|
||||
SpeciesId.AXEW,
|
||||
SpeciesId.FRAXURE,
|
||||
SpeciesId.HAXORUS,
|
||||
SpeciesId.STUNFISK,
|
||||
SpeciesId.DRUDDIGON,
|
||||
SpeciesId.GOLETT,
|
||||
SpeciesId.GOLURK,
|
||||
SpeciesId.DEINO,
|
||||
SpeciesId.ZWEILOUS,
|
||||
SpeciesId.HYDREIGON,
|
||||
SpeciesId.THUNDURUS,
|
||||
SpeciesId.ZEKROM,
|
||||
SpeciesId.MELOETTA,
|
||||
SpeciesId.GENESECT,
|
||||
SpeciesId.BRAIXEN,
|
||||
SpeciesId.DELPHOX,
|
||||
SpeciesId.ESPURR,
|
||||
SpeciesId.MEOWSTIC,
|
||||
SpeciesId.HONEDGE,
|
||||
SpeciesId.DOUBLADE,
|
||||
SpeciesId.AEGISLASH,
|
||||
SpeciesId.SKRELP,
|
||||
SpeciesId.DRAGALGE,
|
||||
SpeciesId.HELIOPTILE,
|
||||
SpeciesId.HELIOLISK,
|
||||
SpeciesId.DEDENNE,
|
||||
SpeciesId.GOOMY,
|
||||
SpeciesId.SLIGGOO,
|
||||
SpeciesId.GOODRA,
|
||||
SpeciesId.ZYGARDE,
|
||||
SpeciesId.HOOPA,
|
||||
SpeciesId.YUNGOOS,
|
||||
SpeciesId.GUMSHOOS,
|
||||
SpeciesId.GRUBBIN,
|
||||
SpeciesId.CHARJABUG,
|
||||
SpeciesId.VIKAVOLT,
|
||||
SpeciesId.PASSIMIAN,
|
||||
SpeciesId.TURTONATOR,
|
||||
SpeciesId.TOGEDEMARU,
|
||||
SpeciesId.DRAMPA,
|
||||
SpeciesId.KOMMO_O,
|
||||
SpeciesId.TAPU_KOKO,
|
||||
SpeciesId.SOLGALEO,
|
||||
SpeciesId.LUNALA,
|
||||
SpeciesId.PHEROMOSA,
|
||||
SpeciesId.XURKITREE,
|
||||
SpeciesId.CELESTEELA,
|
||||
SpeciesId.GUZZLORD,
|
||||
SpeciesId.NECROZMA,
|
||||
SpeciesId.MAGEARNA,
|
||||
SpeciesId.NAGANADEL,
|
||||
SpeciesId.ZERAORA,
|
||||
SpeciesId.TOXTRICITY,
|
||||
SpeciesId.MR_RIME,
|
||||
SpeciesId.REGIELEKI,
|
||||
SpeciesId.WYRDEER,
|
||||
SpeciesId.FARIGIRAF,
|
||||
SpeciesId.DUDUNSPARCE,
|
||||
SpeciesId.MIRAIDON,
|
||||
SpeciesId.RAGING_BOLT,
|
||||
SpeciesId.ALOLA_RATTATA,
|
||||
SpeciesId.ALOLA_RATICATE,
|
||||
SpeciesId.ALOLA_RAICHU,
|
||||
SpeciesId.ALOLA_MEOWTH,
|
||||
SpeciesId.ALOLA_PERSIAN,
|
||||
SpeciesId.ALOLA_GRAVELER,
|
||||
SpeciesId.ALOLA_GOLEM,
|
||||
SpeciesId.ALOLA_GRIMER,
|
||||
SpeciesId.ALOLA_MUK,
|
||||
SpeciesId.GALAR_WEEZING,
|
||||
SpeciesId.GALAR_MR_MIME,
|
||||
SpeciesId.HISUI_SLIGGOO,
|
||||
SpeciesId.HISUI_GOODRA,
|
||||
],
|
||||
[MoveId.WATER_PULSE]: [
|
||||
SpeciesId.SQUIRTLE,
|
||||
SpeciesId.WARTORTLE,
|
||||
@ -68747,6 +69026,7 @@ export const tmPoolTiers: TmPoolTiers = {
|
||||
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA,
|
||||
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT,
|
||||
[MoveId.ROCK_BLAST]: ModifierTier.GREAT,
|
||||
[MoveId.SHOCK_WAVE]: ModifierTier.GREAT,
|
||||
[MoveId.WATER_PULSE]: ModifierTier.GREAT,
|
||||
[MoveId.ROOST]: ModifierTier.GREAT,
|
||||
[MoveId.GRAVITY]: ModifierTier.COMMON,
|
||||
|
@ -67,7 +67,7 @@ import { StatusEffect } from "#enums/status-effect";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { MoveUsedEvent } from "#events/battle-scene";
|
||||
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||
import { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||
import {
|
||||
AttackTypeBoosterModifier,
|
||||
BerryModifier,
|
||||
@ -1005,7 +1005,7 @@ export class AttackMove extends Move {
|
||||
const ret = super.getTargetBenefitScore(user, target, move);
|
||||
let attackScore = 0;
|
||||
|
||||
const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this);
|
||||
const effectiveness = target.getAttackTypeEffectiveness(this.type, {source: user, move: this});
|
||||
attackScore = Math.pow(effectiveness - 1, 2) * (effectiveness < 1 ? -2 : 2);
|
||||
const [ thisStat, offStat ]: EffectiveStat[] = this.category === MoveCategory.PHYSICAL ? [ Stat.ATK, Stat.SPATK ] : [ Stat.SPATK, Stat.ATK ];
|
||||
const statHolder = new NumberHolder(user.getEffectiveStat(thisStat, target));
|
||||
@ -1811,7 +1811,7 @@ export class SacrificialAttr extends MoveEffectAttr {
|
||||
if (user.isBoss()) {
|
||||
return -20;
|
||||
}
|
||||
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5));
|
||||
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, {source: user}) - 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1849,7 +1849,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
|
||||
if (user.isBoss()) {
|
||||
return -20;
|
||||
}
|
||||
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5));
|
||||
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, {source: user}) - 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1891,7 +1891,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
|
||||
if (user.isBoss()) {
|
||||
return -10;
|
||||
}
|
||||
return Math.ceil(((1 - user.getHpRatio() / 2) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5));
|
||||
return Math.ceil(((1 - user.getHpRatio() / 2) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, {source: user}) - 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5365,86 +5365,80 @@ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
||||
if (target.isOfType(PokemonType.FLYING)) {
|
||||
multiplier.value = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr {
|
||||
/**
|
||||
* Checks to see if the Target is Ice-Type or not. If so, the move will have no effect.
|
||||
* @param user n/a
|
||||
* @param target The {@linkcode Pokemon} targeted by the move
|
||||
* @param move n/a
|
||||
* @param args `[0]` a {@linkcode NumberHolder | NumberHolder} containing a type effectiveness multiplier
|
||||
* @returns `true` if this Ice-type immunity applies; `false` otherwise
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
if (target.isOfType(PokemonType.ICE)) {
|
||||
multiplier.value = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, user);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for moves which have a custom type chart interaction.
|
||||
*/
|
||||
export class VariableMoveTypeChartAttr extends MoveAttr {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args [0] {@linkcode NumberHolder} holding the type effectiveness
|
||||
* @param args [1] A single defensive type of the target
|
||||
*
|
||||
* @returns true if application of the attribute succeeds
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param move - The {@linkcode Move} with this attribute
|
||||
* @param args -
|
||||
* `[0]`: A {@linkcode NumberHolder} holding the type effectiveness
|
||||
* `[1]`: The target's entire defensive type profile
|
||||
* `[2]`: The current {@linkcode PokemonType} of the move
|
||||
* @returns `true` if application of the attribute succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class forces Freeze-Dry to be super effective against Water Type.
|
||||
* Attribute to implement {@linkcode MoveId.FREEZE_DRY}'s guaranteed water type super effectiveness.
|
||||
*/
|
||||
export class FreezeDryAttr extends VariableMoveTypeChartAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
const defType = args[1] as PokemonType;
|
||||
|
||||
if (defType === PokemonType.WATER) {
|
||||
multiplier.value = 2;
|
||||
return true;
|
||||
} else {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||
const [multiplier, types, moveType] = args;
|
||||
if (!types.includes(PokemonType.WATER)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Replace whatever the prior "normal" water effectiveness was with a guaranteed 2x multi
|
||||
const normalEff = getTypeDamageMultiplier(moveType, PokemonType.WATER)
|
||||
multiplier.value = 2 * multiplier.value / normalEff;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used by {@linkcode MoveId.THOUSAND_ARROWS} to cause it to deal a fixed 1x damage
|
||||
* against all ungrounded flying types.
|
||||
*/
|
||||
export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||
const [multiplier, types] = args;
|
||||
if (target.isGrounded() || !types.includes(PokemonType.FLYING)) {
|
||||
return false;
|
||||
}
|
||||
multiplier.value = 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used by {@linkcode MoveId.FLYING_PRESS} to add the Flying Type to its type effectiveness.
|
||||
*/
|
||||
export class FlyingTypeMultiplierAttr extends VariableMoveTypeChartAttr {
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||
const multiplier = args[0];
|
||||
multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, {source: user});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used by {@linkcode MoveId.SHEER_COLD} to implement its Gen VII+ ice ineffectiveness.
|
||||
*/
|
||||
export class IceNoEffectTypeAttr extends VariableMoveTypeChartAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||
const [multiplier, types] = args;
|
||||
if (types.includes(PokemonType.ICE)) {
|
||||
multiplier.value = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8132,7 +8126,9 @@ export class UpperHandCondition extends MoveCondition {
|
||||
}
|
||||
}
|
||||
|
||||
export class HitsSameTypeAttr extends VariableMoveTypeMultiplierAttr {
|
||||
// TODO: Does this need to extend from this?
|
||||
// The only reason it might is to show ineffectiveness text but w/e
|
||||
export class HitsSameTypeAttr extends VariableMoveTypeChartAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const multiplier = args[0] as NumberHolder;
|
||||
if (!user.getTypes(true).some(type => target.getTypes(true).includes(type))) {
|
||||
@ -8401,8 +8397,7 @@ const MoveAttrs = Object.freeze({
|
||||
TeraStarstormTypeAttr,
|
||||
MatchUserTypeAttr,
|
||||
CombinedPledgeTypeAttr,
|
||||
VariableMoveTypeMultiplierAttr,
|
||||
NeutralDamageAgainstFlyingTypeMultiplierAttr,
|
||||
NeutralDamageAgainstFlyingTypeAttr,
|
||||
IceNoEffectTypeAttr,
|
||||
FlyingTypeMultiplierAttr,
|
||||
VariableMoveTypeChartAttr,
|
||||
@ -10446,7 +10441,7 @@ export function initMoves() {
|
||||
.attr(HitHealAttr, 0.75)
|
||||
.triageMove(),
|
||||
new AttackMove(MoveId.THOUSAND_ARROWS, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
||||
.attr(NeutralDamageAgainstFlyingTypeAttr)
|
||||
.attr(FallDownAttr)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
||||
@ -11387,9 +11382,10 @@ export function initMoves() {
|
||||
new AttackMove(MoveId.RUINATION, PokemonType.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9)
|
||||
.attr(TargetHalfHpDamageAttr),
|
||||
new AttackMove(MoveId.COLLISION_COURSE, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461 / 4096 : 1),
|
||||
// TODO: Do we want to change this to 4/3?
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, {source: user}) >= 2 ? 5461 / 4096 : 1),
|
||||
new AttackMove(MoveId.ELECTRO_DRIFT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461 / 4096 : 1)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, {source: user}) >= 2 ? 5461 / 4096 : 1)
|
||||
.makesContact(),
|
||||
new SelfStatusMove(MoveId.SHED_TAIL, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(AddSubstituteAttr, 0.5, true)
|
||||
|
@ -99,6 +99,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
|
||||
MysteryEncounterType.DARK_DEAL,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.ROGUE)
|
||||
.withDisallowedChallenges(Challenges.HARDCORE)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "dark_deal_scientist",
|
||||
|
@ -673,6 +673,8 @@ export async function catchPokemon(
|
||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
const doPokemonCatchMenu = () => {
|
||||
const end = () => {
|
||||
// Ensure the pokemon is in the enemy party in all situations
|
||||
@ -708,9 +710,7 @@ export async function catchPokemon(
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
if (!addStatus.value) {
|
||||
if (!(isObtain || addStatus.value)) {
|
||||
removePokemon();
|
||||
end();
|
||||
return;
|
||||
@ -807,10 +807,16 @@ export async function catchPokemon(
|
||||
};
|
||||
|
||||
if (showCatchObtainMessage) {
|
||||
let catchMessage: string;
|
||||
if (isObtain) {
|
||||
catchMessage = "battle:pokemonObtained";
|
||||
} else if (addStatus.value) {
|
||||
catchMessage = "battle:pokemonCaught";
|
||||
} else {
|
||||
catchMessage = "battle:pokemonCaughtButChallenge";
|
||||
}
|
||||
globalScene.ui.showText(
|
||||
i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", {
|
||||
pokemonName: pokemon.getNameToRender(),
|
||||
}),
|
||||
i18next.t(catchMessage, { pokemonName: pokemon.getNameToRender() }),
|
||||
null,
|
||||
doPokemonCatchMenu,
|
||||
0,
|
||||
|
@ -2,6 +2,13 @@ import { PokemonType } from "#enums/pokemon-type";
|
||||
|
||||
export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8;
|
||||
|
||||
/**
|
||||
* Get the type effectiveness multiplier of one PokemonType against another.
|
||||
* @param attackType - The {@linkcode PokemonType} of the attacker
|
||||
* @param defType - The {@linkcode PokemonType} of the defender
|
||||
* @returns The type damage multiplier between the two types;
|
||||
* will be either `0`, `0.5`, `1` or `2`.
|
||||
*/
|
||||
export function getTypeDamageMultiplier(attackType: PokemonType, defType: PokemonType): TypeDamageMultiplier {
|
||||
if (attackType === PokemonType.UNKNOWN || defType === PokemonType.UNKNOWN) {
|
||||
return 1;
|
||||
|
@ -130,7 +130,8 @@ import {
|
||||
TempStatStageBoosterModifier,
|
||||
} from "#modifiers/modifier";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import type { Move } from "#moves/move";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
import type { Move, VariableMoveTypeChartAttr } from "#moves/move";
|
||||
import { getMoveTargets } from "#moves/move-utils";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { loadMoveAnimations } from "#sprites/pokemon-asset-loader";
|
||||
@ -207,6 +208,38 @@ type getBaseDamageParams = Omit<damageParams, "effectiveness">;
|
||||
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
|
||||
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
|
||||
|
||||
/**
|
||||
* Type for the parameters of {@linkcode Pokemon.getAttackTypeEffectiveness | getAttackTypeEffectiveness}
|
||||
* and associated helper functions.
|
||||
*/
|
||||
type getAttackTypeEffectivenessParams = {
|
||||
/**
|
||||
* The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities
|
||||
* and the effects of Foresight/Odor Sleuth.
|
||||
*/
|
||||
source?: Pokemon;
|
||||
/**
|
||||
* If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks)
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
ignoreStrongWinds?: boolean;
|
||||
/**
|
||||
* If `true`, will prevent changes to game state during calculations.
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
simulated?: boolean;
|
||||
/**
|
||||
* The {@linkcode Move} whose type effectiveness is being checked.
|
||||
* Used for applying {@linkcode VariableMoveTypeChartAttr}
|
||||
*/
|
||||
move?: Move;
|
||||
/**
|
||||
* Whether to consider this Pokemon's {@linkcode IllusionData | illusion} when determining types.
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
useIllusion?: boolean;
|
||||
};
|
||||
|
||||
export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
|
||||
@ -2399,11 +2432,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
const typeMultiplier = new NumberHolder(
|
||||
move.category !== MoveCategory.STATUS || move.hasAttr("RespectAttackTypeImmunityAttr")
|
||||
? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move, useIllusion)
|
||||
? this.getAttackTypeEffectiveness(moveType, { source, simulated, move, useIllusion })
|
||||
: 1,
|
||||
);
|
||||
|
||||
applyMoveAttrs("VariableMoveTypeMultiplierAttr", source, this, move, typeMultiplier);
|
||||
if (this.getTypes(true, true).find(t => move.isTypeImmune(source, this, t))) {
|
||||
typeMultiplier.value = 0;
|
||||
}
|
||||
@ -2463,26 +2495,31 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the move's type effectiveness multiplier based on the target's type/s.
|
||||
* @param moveType {@linkcode PokemonType} the type of the move being used
|
||||
* @param source {@linkcode Pokemon} the Pokemon using the move
|
||||
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
||||
* @param simulated tag to only apply the strong winds effect message when the move is used
|
||||
* @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
|
||||
* @param useIllusion - Whether we want the attack type effectiveness on the illusion or not
|
||||
* @returns a multiplier for the type effectiveness
|
||||
* Calculate the type effectiveness multiplier of a Move used **against** this Pokemon.
|
||||
* @param moveType - The {@linkcode PokemonType} of the move being used
|
||||
* @param source - The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities
|
||||
* and the effects of Foresight/Odor Sleuth
|
||||
* @param ignoreStrongWinds - If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks);
|
||||
* default `false`
|
||||
* @param simulated - If `true`, will prevent changes to game state during calculations; default `false`
|
||||
* @param move - The {@linkcode Move} whose type effectiveness is being checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
|
||||
* @param useIllusion - Whether to consider this Pokemon's {@linkcode IllusionData | illusion} when determining types; default `false`
|
||||
* @returns The computed type effectiveness multiplier.
|
||||
*/
|
||||
getAttackTypeEffectiveness(
|
||||
moveType: PokemonType,
|
||||
source?: Pokemon,
|
||||
ignoreStrongWinds = false,
|
||||
simulated = true,
|
||||
move?: Move,
|
||||
useIllusion = false,
|
||||
{
|
||||
source,
|
||||
ignoreStrongWinds = false,
|
||||
simulated = true,
|
||||
move,
|
||||
useIllusion = false,
|
||||
}: getAttackTypeEffectivenessParams = {},
|
||||
): TypeDamageMultiplier {
|
||||
if (moveType === PokemonType.STELLAR) {
|
||||
return this.isTerastallized ? 2 : 1;
|
||||
}
|
||||
|
||||
const types = this.getTypes(true, true, undefined, useIllusion);
|
||||
const arena = globalScene.arena;
|
||||
|
||||
@ -2495,57 +2532,79 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
let multiplier = types
|
||||
.map(defenderType => {
|
||||
const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
||||
if (move) {
|
||||
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defenderType);
|
||||
}
|
||||
if (source) {
|
||||
const ignoreImmunity = new BooleanHolder(false);
|
||||
if (source.isActive(true) && source.hasAbilityWithAttr("IgnoreTypeImmunityAbAttr")) {
|
||||
applyAbAttrs("IgnoreTypeImmunityAbAttr", {
|
||||
pokemon: source,
|
||||
cancelled: ignoreImmunity,
|
||||
simulated,
|
||||
moveType,
|
||||
defenderType,
|
||||
});
|
||||
}
|
||||
if (ignoreImmunity.value) {
|
||||
if (multiplier.value === 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const multi = new NumberHolder(1);
|
||||
for (const defenderType of types) {
|
||||
const typeMulti = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, typeMulti);
|
||||
// If the target is immune to the type in question, check for any effects that would ignore said effect
|
||||
// TODO: Review if the `isActive` check is needed anymore
|
||||
if (
|
||||
source?.isActive(true) &&
|
||||
typeMulti.value === 0 &&
|
||||
this.checkIgnoreTypeImmunity({ source, simulated, moveType, defenderType })
|
||||
) {
|
||||
typeMulti.value = 1;
|
||||
}
|
||||
multi.value *= typeMulti.value;
|
||||
}
|
||||
|
||||
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
|
||||
if (exposedTags.some(t => t.ignoreImmunity(defenderType, moveType))) {
|
||||
if (multiplier.value === 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return multiplier.value;
|
||||
})
|
||||
.reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
|
||||
// Apply any typing changes from Freeze-Dry, etc.
|
||||
if (move) {
|
||||
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multi, types, moveType);
|
||||
}
|
||||
|
||||
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
||||
const typeMultiplierAgainstFlying = new NumberHolder(getTypeDamageMultiplier(moveType, PokemonType.FLYING));
|
||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, typeMultiplierAgainstFlying);
|
||||
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
||||
if (
|
||||
!ignoreStrongWinds &&
|
||||
arena.weather?.weatherType === WeatherType.STRONG_WINDS &&
|
||||
!arena.weather.isEffectSuppressed() &&
|
||||
this.isOfType(PokemonType.FLYING) &&
|
||||
arena.getWeatherType() === WeatherType.STRONG_WINDS &&
|
||||
!arena.weather?.isEffectSuppressed() &&
|
||||
types.includes(PokemonType.FLYING) &&
|
||||
typeMultiplierAgainstFlying.value === 2
|
||||
) {
|
||||
multiplier /= 2;
|
||||
multi.value /= 2;
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
||||
}
|
||||
}
|
||||
return multiplier as TypeDamageMultiplier;
|
||||
return multi.value as TypeDamageMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-method of {@linkcode getAttackTypeEffectiveness} that handles nullifying type immunities.
|
||||
* @param source - The {@linkcode Pokemon} from whom the attack is sourced
|
||||
* @param simulated - If `true`, will prevent displaying messages upon activation
|
||||
* @param moveType - The {@linkcode PokemonType} whose offensive typing is being checked
|
||||
* @param defenderType - The defender's {@linkcode PokemonType} being checked
|
||||
* @returns Whether the type immunity was bypassed
|
||||
*/
|
||||
private checkIgnoreTypeImmunity({
|
||||
source,
|
||||
simulated,
|
||||
moveType,
|
||||
defenderType,
|
||||
}: {
|
||||
source: Pokemon;
|
||||
simulated: boolean;
|
||||
moveType: PokemonType;
|
||||
defenderType: PokemonType;
|
||||
}): boolean {
|
||||
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
|
||||
const hasExposed = exposedTags.some(t => t.ignoreImmunity(defenderType, moveType));
|
||||
if (hasExposed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ignoreImmunity = new BooleanHolder(false);
|
||||
applyAbAttrs("IgnoreTypeImmunityAbAttr", {
|
||||
pokemon: source,
|
||||
cancelled: ignoreImmunity,
|
||||
simulated,
|
||||
moveType,
|
||||
defenderType,
|
||||
});
|
||||
return ignoreImmunity.value;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2565,10 +2624,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* Based on how effectively this Pokemon defends against the opponent's types.
|
||||
* This score cannot be higher than 4.
|
||||
*/
|
||||
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], opponent), 0.25);
|
||||
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], { source: opponent }), 0.25);
|
||||
if (enemyTypes.length > 1) {
|
||||
defScore *=
|
||||
1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25);
|
||||
// TODO: Shouldn't this pass `simulated=true` here?
|
||||
1 /
|
||||
Math.max(
|
||||
this.getAttackTypeEffectiveness(enemyTypes[1], { source: opponent, simulated: false, useIllusion: true }),
|
||||
0.25,
|
||||
);
|
||||
}
|
||||
|
||||
const moveset = this.moveset;
|
||||
@ -2582,7 +2646,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
continue;
|
||||
}
|
||||
const moveType = resolvedMove.type;
|
||||
let thisScore = opponent.getAttackTypeEffectiveness(moveType, this, false, true, undefined, true);
|
||||
let thisScore = opponent.getAttackTypeEffectiveness(moveType, {
|
||||
source: this,
|
||||
simulated: true,
|
||||
useIllusion: true,
|
||||
});
|
||||
|
||||
// Add STAB multiplier for attack type effectiveness.
|
||||
// For now, simply don't apply STAB to moves that may change type
|
||||
|
@ -62,15 +62,24 @@ export class GameMode implements GameModeConfig {
|
||||
|
||||
/**
|
||||
* Enables challenges if they are disabled and sets the specified challenge's value
|
||||
* @param challenge The challenge to set
|
||||
* @param value The value to give the challenge. Impact depends on the specific challenge
|
||||
* @param challenge - The challenge to set
|
||||
* @param value - The value to give the challenge. Impact depends on the specific challenge
|
||||
* @param severity - If provided, will override the given severity amount. Unused if `challenge` does not use severity
|
||||
* @todo Add severity support to daily mode challenge setting
|
||||
*/
|
||||
setChallengeValue(challenge: Challenges, value: number) {
|
||||
setChallengeValue(challenge: Challenges, value: number, severity?: number) {
|
||||
if (!this.isChallenge) {
|
||||
this.isChallenge = true;
|
||||
this.challenges = allChallenges.map(c => copyChallenge(c));
|
||||
}
|
||||
this.challenges.filter((chal: Challenge) => chal.id === challenge).map((chal: Challenge) => (chal.value = value));
|
||||
this.challenges
|
||||
.filter((chal: Challenge) => chal.id === challenge)
|
||||
.forEach(chal => {
|
||||
chal.value = value;
|
||||
if (chal.hasSeverity()) {
|
||||
chal.severity = severity ?? chal.severity;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -253,8 +253,11 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
|
||||
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
|
||||
globalScene.ui.showText(
|
||||
i18next.t("battle:pokemonCaught", {
|
||||
i18next.t(addStatus.value ? "battle:pokemonCaught" : "battle:pokemonCaughtButChallenge", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
null,
|
||||
@ -290,8 +293,6 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
const addStatus = new BooleanHolder(true);
|
||||
applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus);
|
||||
if (!addStatus.value) {
|
||||
removePokemon();
|
||||
end();
|
||||
|
@ -16,8 +16,10 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
|
||||
globalScene.resetSeed();
|
||||
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentBiome = globalScene.arena.biomeType;
|
||||
const nextWaveIndex = globalScene.currentBattle.waveIndex + 1;
|
||||
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||
const nextWaveIndex = currentWaveIndex + 1;
|
||||
|
||||
const setNextBiome = (nextBiome: BiomeId) => {
|
||||
if (nextWaveIndex % 10 === 1) {
|
||||
@ -26,6 +28,15 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||
if (healStatus.value) {
|
||||
globalScene.phaseManager.unshiftNew("PartyHealPhase", false);
|
||||
} else {
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"SelectModifierPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
gameMode.isFixedBattle(currentWaveIndex)
|
||||
? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome);
|
||||
@ -33,12 +44,12 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
};
|
||||
|
||||
if (
|
||||
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||
(globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||
(gameMode.isClassic && gameMode.isWaveFinal(nextWaveIndex + 9)) ||
|
||||
(gameMode.isDaily && gameMode.isWaveFinal(nextWaveIndex)) ||
|
||||
(gameMode.hasShortBiomes && !(nextWaveIndex % 50))
|
||||
) {
|
||||
setNextBiome(BiomeId.END);
|
||||
} else if (globalScene.gameMode.hasRandomBiomes) {
|
||||
} else if (gameMode.hasRandomBiomes) {
|
||||
setNextBiome(this.generateNextBiome(nextWaveIndex));
|
||||
} else if (Array.isArray(biomeLinks[currentBiome])) {
|
||||
const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[])
|
||||
@ -73,9 +84,6 @@ export class SelectBiomePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
generateNextBiome(waveIndex: number): BiomeId {
|
||||
if (!(waveIndex % 50)) {
|
||||
return BiomeId.END;
|
||||
}
|
||||
return globalScene.generateRandomBiome(waveIndex);
|
||||
return waveIndex % 50 === 0 ? BiomeId.END : globalScene.generateRandomBiome(waveIndex);
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,9 @@ import { globalScene } from "#app/global-scene";
|
||||
import { modifierTypes } from "#data/data-lists";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { ChallengeType } from "#enums/challenge-type";
|
||||
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
||||
import type { CustomModifierSettings } from "#modifiers/modifier-type";
|
||||
import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { BooleanHolder } from "#utils/common";
|
||||
|
||||
export class VictoryPhase extends PokemonPhase {
|
||||
public readonly phaseName = "VictoryPhase";
|
||||
@ -49,15 +45,19 @@ export class VictoryPhase extends PokemonPhase {
|
||||
if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
|
||||
globalScene.phaseManager.pushNew("TrainerVictoryPhase");
|
||||
}
|
||||
if (globalScene.gameMode.isEndless || !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
||||
|
||||
const gameMode = globalScene.gameMode;
|
||||
const currentWaveIndex = globalScene.currentBattle.waveIndex;
|
||||
|
||||
if (gameMode.isEndless || !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||
globalScene.phaseManager.pushNew("EggLapsePhase");
|
||||
if (globalScene.gameMode.isClassic) {
|
||||
switch (globalScene.currentBattle.waveIndex) {
|
||||
if (gameMode.isClassic) {
|
||||
switch (currentWaveIndex) {
|
||||
case ClassicFixedBossWaves.RIVAL_1:
|
||||
case ClassicFixedBossWaves.RIVAL_2:
|
||||
// Get event modifiers for this wave
|
||||
timedEventManager
|
||||
.getFixedBattleEventRewards(globalScene.currentBattle.waveIndex)
|
||||
.getFixedBattleEventRewards(currentWaveIndex)
|
||||
.map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r]));
|
||||
break;
|
||||
case ClassicFixedBossWaves.EVIL_BOSS_2:
|
||||
@ -66,59 +66,53 @@ export class VictoryPhase extends PokemonPhase {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0);
|
||||
applyChallenges(ChallengeType.PARTY_HEAL, healStatus);
|
||||
if (!healStatus.value) {
|
||||
if (currentWaveIndex % 10) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"SelectModifierPhase",
|
||||
undefined,
|
||||
undefined,
|
||||
this.getFixedBattleCustomModifiers(),
|
||||
gameMode.isFixedBattle(currentWaveIndex)
|
||||
? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings
|
||||
: undefined,
|
||||
);
|
||||
} else if (globalScene.gameMode.isDaily) {
|
||||
} else if (gameMode.isDaily) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM);
|
||||
if (
|
||||
globalScene.currentBattle.waveIndex > 10 &&
|
||||
!globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)
|
||||
) {
|
||||
if (currentWaveIndex > 10 && !gameMode.isWaveFinal(currentWaveIndex)) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||
}
|
||||
} else {
|
||||
const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||
if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) {
|
||||
const superExpWave = !gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10;
|
||||
if (gameMode.isEndless && currentWaveIndex === 10) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE);
|
||||
}
|
||||
if (
|
||||
globalScene.currentBattle.waveIndex <= 750 &&
|
||||
(globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave)
|
||||
) {
|
||||
if (currentWaveIndex <= 750 && (currentWaveIndex <= 500 || currentWaveIndex % 30 === superExpWave)) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"ModifierRewardPhase",
|
||||
globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250
|
||||
currentWaveIndex % 30 !== superExpWave || currentWaveIndex > 250
|
||||
? modifierTypes.EXP_CHARM
|
||||
: modifierTypes.SUPER_EXP_CHARM,
|
||||
);
|
||||
}
|
||||
if (globalScene.currentBattle.waveIndex <= 150 && !(globalScene.currentBattle.waveIndex % 50)) {
|
||||
if (currentWaveIndex <= 150 && !(currentWaveIndex % 50)) {
|
||||
globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL);
|
||||
}
|
||||
if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) {
|
||||
if (gameMode.isEndless && !(currentWaveIndex % 50)) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"ModifierRewardPhase",
|
||||
!(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||
!(currentWaveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS,
|
||||
);
|
||||
globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase");
|
||||
}
|
||||
}
|
||||
|
||||
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
if (gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
|
||||
globalScene.phaseManager.pushNew("SelectBiomePhase");
|
||||
}
|
||||
|
||||
globalScene.phaseManager.pushNew("NewBattlePhase");
|
||||
} else {
|
||||
globalScene.currentBattle.battleType = BattleType.CLEAR;
|
||||
globalScene.score += globalScene.gameMode.getClearScoreBonus();
|
||||
globalScene.score += gameMode.getClearScoreBonus();
|
||||
globalScene.updateScoreText();
|
||||
globalScene.phaseManager.pushNew("GameOverPhase", true);
|
||||
}
|
||||
@ -126,18 +120,4 @@ export class VictoryPhase extends PokemonPhase {
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* If this wave is a fixed battle with special custom modifier rewards,
|
||||
* will pass those settings to the upcoming {@linkcode SelectModifierPhase}`.
|
||||
*/
|
||||
getFixedBattleCustomModifiers(): CustomModifierSettings | undefined {
|
||||
const gameMode = globalScene.gameMode;
|
||||
const waveIndex = globalScene.currentBattle.waveIndex;
|
||||
if (gameMode.isFixedBattle(waveIndex)) {
|
||||
return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -448,6 +448,8 @@ export function getAchievementDescription(localizationKey: string): string {
|
||||
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
|
||||
case "FLIP_INVERSE":
|
||||
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
|
||||
case "NUZLOCKE":
|
||||
return i18next.t("achv:NUZLOCKE.description", { context: genderStr });
|
||||
case "BREEDERS_IN_SPACE":
|
||||
return i18next.t("achv:BREEDERS_IN_SPACE.description", {
|
||||
context: genderStr,
|
||||
|
@ -208,6 +208,26 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
);
|
||||
this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant);
|
||||
this.add(this.icon);
|
||||
|
||||
[
|
||||
this.hiddenAbilityIcon,
|
||||
this.favoriteIcon,
|
||||
this.classicWinIcon,
|
||||
this.candyUpgradeIcon,
|
||||
this.candyUpgradeOverlayIcon,
|
||||
this.eggMove1Icon,
|
||||
this.tmMove1Icon,
|
||||
this.eggMove2Icon,
|
||||
this.tmMove2Icon,
|
||||
this.passive1Icon,
|
||||
this.passive2Icon,
|
||||
this.passive1OverlayIcon,
|
||||
this.passive2OverlayIcon,
|
||||
].forEach(icon => {
|
||||
if (icon) {
|
||||
this.bringToTop(icon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkIconId(female, formIndex, shiny, variant) {
|
||||
|
@ -410,6 +410,11 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
new DropDownLabel(i18next.t("filterBar:hasHiddenAbility"), undefined, DropDownState.ON),
|
||||
new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE),
|
||||
];
|
||||
const seenSpeciesLabels = [
|
||||
new DropDownLabel(i18next.t("filterBar:seenSpecies"), undefined, DropDownState.OFF),
|
||||
new DropDownLabel(i18next.t("filterBar:isSeen"), undefined, DropDownState.ON),
|
||||
new DropDownLabel(i18next.t("filterBar:isUnseen"), undefined, DropDownState.EXCLUDE),
|
||||
];
|
||||
const eggLabels = [
|
||||
new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF),
|
||||
new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON),
|
||||
@ -423,6 +428,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
new DropDownOption("FAVORITE", favoriteLabels),
|
||||
new DropDownOption("WIN", winLabels),
|
||||
new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels),
|
||||
new DropDownOption("SEEN_SPECIES", seenSpeciesLabels),
|
||||
new DropDownOption("EGG", eggLabels),
|
||||
new DropDownOption("POKERUS", pokerusLabels),
|
||||
];
|
||||
@ -792,13 +798,15 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
|
||||
}
|
||||
|
||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean {
|
||||
isSeen(species: PokemonSpecies, dexEntry: DexEntry, seenFilter?: boolean): boolean {
|
||||
if (dexEntry?.seenAttr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||
return !!starterDexEntry?.caughtAttr;
|
||||
if (!seenFilter) {
|
||||
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
|
||||
return !!starterDexEntry?.caughtAttr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1617,6 +1625,21 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
}
|
||||
});
|
||||
|
||||
// Seen Filter
|
||||
const dexEntry = globalScene.gameData.dexData[species.speciesId];
|
||||
const isItSeen = this.isSeen(species, dexEntry, true);
|
||||
const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) {
|
||||
return isItSeen;
|
||||
}
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.EXCLUDE) {
|
||||
return !isItSeen;
|
||||
}
|
||||
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.OFF) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Egg Purchasable Filter
|
||||
const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId);
|
||||
const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
|
||||
@ -1658,6 +1681,7 @@ export class PokedexUiHandler extends MessageUiHandler {
|
||||
fitsFavorite &&
|
||||
fitsWin &&
|
||||
fitsHA &&
|
||||
fitsSeen &&
|
||||
fitsEgg &&
|
||||
fitsPokerus
|
||||
) {
|
||||
|
@ -210,7 +210,8 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
this.runContainer.add(headerText);
|
||||
const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW);
|
||||
runName.setOrigin(0, 0);
|
||||
runName.setPositionRelative(headerBg, 60, 4);
|
||||
const runNameX = headerText.width / 6 + headerText.x + 4;
|
||||
runName.setPositionRelative(headerBg, runNameX, 4);
|
||||
this.runContainer.add(runName);
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,7 @@ describe("Abilities - Illusion", () => {
|
||||
expect(game.field.getPlayerPokemon().summonData.illusion).toBeFalsy();
|
||||
});
|
||||
|
||||
// TODO: This doesn't actually check that the ai calls the function this way... useless test
|
||||
it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => {
|
||||
game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]);
|
||||
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]);
|
||||
@ -97,22 +98,16 @@ describe("Abilities - Illusion", () => {
|
||||
|
||||
const flameThrower = enemy.getMoveset()[0]!.getMove();
|
||||
const psychic = enemy.getMoveset()[1]!.getMove();
|
||||
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(
|
||||
flameThrower.type,
|
||||
enemy,
|
||||
undefined,
|
||||
undefined,
|
||||
flameThrower,
|
||||
true,
|
||||
);
|
||||
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(
|
||||
psychic.type,
|
||||
enemy,
|
||||
undefined,
|
||||
undefined,
|
||||
psychic,
|
||||
true,
|
||||
);
|
||||
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(flameThrower.type, {
|
||||
source: enemy,
|
||||
move: flameThrower,
|
||||
useIllusion: true,
|
||||
});
|
||||
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(psychic.type, {
|
||||
source: enemy,
|
||||
move: psychic,
|
||||
useIllusion: true,
|
||||
});
|
||||
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
@ -113,4 +114,18 @@ describe("Abilities - Tera Shell", () => {
|
||||
}
|
||||
expect(spy).toHaveReturnedTimes(2);
|
||||
});
|
||||
|
||||
it("should overwrite Freeze-Dry", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.TERAPAGOS]);
|
||||
|
||||
const terapagos = game.field.getPlayerPokemon();
|
||||
terapagos.summonData.types = [PokemonType.WATER];
|
||||
const spy = vi.spyOn(terapagos, "getMoveEffectiveness");
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.FREEZE_DRY);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(spy).toHaveLastReturnedWith(0.5);
|
||||
});
|
||||
});
|
||||
|
@ -42,7 +42,7 @@ describe("Weather - Strong Winds", () => {
|
||||
game.move.select(MoveId.THUNDERBOLT);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, pikachu)).toBe(0.5);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, { source: pikachu })).toBe(0.5);
|
||||
});
|
||||
|
||||
it("electric type move is neutral for flying type pokemon", async () => {
|
||||
@ -53,7 +53,7 @@ describe("Weather - Strong Winds", () => {
|
||||
game.move.select(MoveId.THUNDERBOLT);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, pikachu)).toBe(1);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, { source: pikachu })).toBe(1);
|
||||
});
|
||||
|
||||
it("ice type move is neutral for flying type pokemon", async () => {
|
||||
@ -64,7 +64,7 @@ describe("Weather - Strong Winds", () => {
|
||||
game.move.select(MoveId.ICE_BEAM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ICE_BEAM].type, pikachu)).toBe(1);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ICE_BEAM].type, { source: pikachu })).toBe(1);
|
||||
});
|
||||
|
||||
it("rock type move is neutral for flying type pokemon", async () => {
|
||||
@ -75,7 +75,7 @@ describe("Weather - Strong Winds", () => {
|
||||
game.move.select(MoveId.ROCK_SLIDE);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ROCK_SLIDE].type, pikachu)).toBe(1);
|
||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ROCK_SLIDE].type, { source: pikachu })).toBe(1);
|
||||
});
|
||||
|
||||
it("weather goes away when last trainer pokemon dies to indirect damage", async () => {
|
||||
|
@ -106,21 +106,6 @@ describe("Inverse Battle", () => {
|
||||
expect(currentHp).toBeGreaterThan((maxHp * 31) / 32 - 1);
|
||||
});
|
||||
|
||||
it("Freeze Dry is 2x effective against Water Type like other Ice type Move - Freeze Dry against Squirtle", async () => {
|
||||
game.override.moveset([MoveId.FREEZE_DRY]).enemySpecies(SpeciesId.SQUIRTLE);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("Water Absorb should heal against water moves - Water Absorb against Water gun", async () => {
|
||||
game.override.moveset([MoveId.WATER_GUN]).enemyAbility(AbilityId.WATER_ABSORB);
|
||||
|
||||
@ -164,6 +149,7 @@ describe("Inverse Battle", () => {
|
||||
expect(enemy.status?.effect).not.toBe(StatusEffect.PARALYSIS);
|
||||
});
|
||||
|
||||
// TODO: These should belong to their respective moves' test files, not the inverse battle mechanic itself
|
||||
it("Ground type is not immune to Thunder Wave - Thunder Wave against Sandshrew", async () => {
|
||||
game.override.moveset([MoveId.THUNDER_WAVE]).enemySpecies(SpeciesId.SANDSHREW);
|
||||
|
||||
@ -202,21 +188,6 @@ describe("Inverse Battle", () => {
|
||||
expect(player.getTypes()[0]).toBe(PokemonType.DRAGON);
|
||||
});
|
||||
|
||||
it("Flying Press should be 0.25x effective against Grass + Dark Type - Flying Press against Meowscarada", async () => {
|
||||
game.override.moveset([MoveId.FLYING_PRESS]).enemySpecies(SpeciesId.MEOWSCARADA);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FLYING_PRESS);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(0.25);
|
||||
});
|
||||
|
||||
it("Scrappy ability has no effect - Tackle against Ghost Type still 2x effective with Scrappy", async () => {
|
||||
game.override.moveset([MoveId.TACKLE]).ability(AbilityId.SCRAPPY).enemySpecies(SpeciesId.GASTLY);
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -52,4 +54,18 @@ describe("Challenges - Limited Catch", () => {
|
||||
|
||||
expect(game.scene.getPlayerParty()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should allow gift Pokémon from Mystery Encounters to be added to party", async () => {
|
||||
game.override
|
||||
.mysteryEncounterChance(100)
|
||||
.mysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN)
|
||||
.startingWave(12);
|
||||
game.scene.money = 20000;
|
||||
|
||||
await game.challengeMode.runToSummon([SpeciesId.NUZLEAF]);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
|
||||
expect(game.scene.getPlayerParty()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { ExpBoosterModifier } from "#modifiers/modifier";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
|
||||
import Phaser from "phaser";
|
||||
@ -75,6 +76,7 @@ describe("Challenges - Limited Support", () => {
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
expect(game.scene.getModifiers(ExpBoosterModifier)).toHaveLength(1);
|
||||
expect(playerPokemon).not.toHaveFullHp();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
|
131
test/moves/flying-press.test.ts
Normal file
131
test/moves/flying-press.test.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { allAbilities, allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import Phaser from "phaser";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
describe.sequential("Move - Flying Press", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let hawlucha: PlayerPokemon;
|
||||
let enemy: EnemyPokemon;
|
||||
|
||||
beforeAll(async () => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.HAWLUCHA]);
|
||||
|
||||
hawlucha = game.field.getPlayerPokemon();
|
||||
enemy = game.field.getEnemyPokemon();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
// Reset temp data after each test
|
||||
afterEach(() => {
|
||||
hawlucha.resetSummonData();
|
||||
enemy.resetSummonData();
|
||||
});
|
||||
|
||||
const pokemonTypes = getEnumValues(PokemonType);
|
||||
|
||||
function checkEffForAllTypes(primaryType: PokemonType) {
|
||||
for (const type of pokemonTypes) {
|
||||
enemy.summonData.types = [type];
|
||||
const primaryEff = enemy.getAttackTypeEffectiveness(primaryType, { source: hawlucha });
|
||||
const flyingEff = enemy.getAttackTypeEffectiveness(PokemonType.FLYING, { source: hawlucha });
|
||||
const flyingPressEff = enemy.getAttackTypeEffectiveness(hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]), {
|
||||
source: hawlucha,
|
||||
move: allMoves[MoveId.FLYING_PRESS],
|
||||
});
|
||||
expect
|
||||
.soft(
|
||||
flyingPressEff,
|
||||
`Flying Press effectiveness against ${toTitleCase(PokemonType[type])} was incorrect!` +
|
||||
`\nExpected: ${flyingPressEff},` +
|
||||
`\nActual: ${primaryEff * flyingEff} (=${primaryEff} * ${flyingEff})`,
|
||||
)
|
||||
.toBe(primaryEff * flyingEff);
|
||||
}
|
||||
}
|
||||
|
||||
describe("Normal -", () => {
|
||||
it("should deal damage as a Fighting/Flying type move by default", async () => {
|
||||
checkEffForAllTypes(PokemonType.FIGHTING);
|
||||
});
|
||||
|
||||
it("should deal damage as an Electric/Flying type move when Electrify is active", async () => {
|
||||
hawlucha.addTag(BattlerTagType.ELECTRIFIED);
|
||||
checkEffForAllTypes(PokemonType.ELECTRIC);
|
||||
});
|
||||
|
||||
it("should deal damage as a Normal/Flying type move when Normalize is active", async () => {
|
||||
hawlucha.setTempAbility(allAbilities[AbilityId.NORMALIZE]);
|
||||
checkEffForAllTypes(PokemonType.NORMAL);
|
||||
});
|
||||
|
||||
it("should deal 8x damage against a Normal/Ice type with Grass added", () => {
|
||||
enemy.summonData.types = [PokemonType.NORMAL, PokemonType.ICE];
|
||||
enemy.summonData.addedType = PokemonType.GRASS;
|
||||
|
||||
const moveType = hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]);
|
||||
const flyingPressEff = enemy.getAttackTypeEffectiveness(moveType, {
|
||||
source: hawlucha,
|
||||
move: allMoves[MoveId.FLYING_PRESS],
|
||||
});
|
||||
expect(flyingPressEff).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Inverse Battle -", () => {
|
||||
beforeAll(() => {
|
||||
game.challengeMode.overrideGameWithChallenges(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
});
|
||||
|
||||
it("should deal damage as a Fighting/Flying type move by default", async () => {
|
||||
checkEffForAllTypes(PokemonType.FIGHTING);
|
||||
});
|
||||
|
||||
it("should deal damage as an Electric/Flying type move when Electrify is active", async () => {
|
||||
hawlucha.addTag(BattlerTagType.ELECTRIFIED);
|
||||
checkEffForAllTypes(PokemonType.ELECTRIC);
|
||||
});
|
||||
|
||||
it("should deal damage as a Normal/Flying type move when Normalize is active", async () => {
|
||||
hawlucha.setTempAbility(allAbilities[AbilityId.NORMALIZE]);
|
||||
checkEffForAllTypes(PokemonType.NORMAL);
|
||||
});
|
||||
|
||||
it("should deal 0.125x damage against a Normal/Ice type with Grass added", () => {
|
||||
enemy.summonData.types = [PokemonType.NORMAL, PokemonType.ICE];
|
||||
enemy.summonData.addedType = PokemonType.GRASS;
|
||||
|
||||
const moveType = hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]);
|
||||
const flyingPressEff = enemy.getAttackTypeEffectiveness(moveType, {
|
||||
source: hawlucha,
|
||||
move: allMoves[MoveId.FLYING_PRESS],
|
||||
});
|
||||
expect(flyingPressEff).toBe(0.125);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,330 +1,140 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import type { TypeDamageMultiplier } from "#data/type";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { stringifyEnumArray } from "#test/test-utils/string-utils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Freeze-Dry", () => {
|
||||
type typesArray = [PokemonType] | [PokemonType, PokemonType] | [PokemonType, PokemonType, PokemonType];
|
||||
|
||||
describe.sequential("Move - Freeze-Dry", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
beforeAll(() => {
|
||||
let feebas: PlayerPokemon;
|
||||
let enemy: EnemyPokemon;
|
||||
|
||||
beforeAll(async () => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.starterSpecies(SpeciesId.FEEBAS)
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.moveset([MoveId.FREEZE_DRY, MoveId.FORESTS_CURSE, MoveId.SOAK]);
|
||||
.ability(AbilityId.BALL_FETCH);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
feebas = game.field.getPlayerPokemon();
|
||||
enemy = game.field.getEnemyPokemon();
|
||||
});
|
||||
|
||||
it("should deal 2x damage to pure water types", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
// Reset temp data after each test
|
||||
afterEach(() => {
|
||||
feebas.resetSummonData();
|
||||
enemy.resetSummonData();
|
||||
enemy.isTerastallized = false;
|
||||
});
|
||||
|
||||
it("should deal 4x damage to water/flying types", async () => {
|
||||
game.override.enemySpecies(SpeciesId.WINGULL);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4);
|
||||
});
|
||||
|
||||
it("should deal 1x damage to water/fire types", async () => {
|
||||
game.override.enemySpecies(SpeciesId.VOLCANION);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1);
|
||||
afterAll(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
/**
|
||||
* Freeze drys forced super effectiveness should overwrite wonder guard
|
||||
* Check that Freeze-Dry is the given effectiveness against the given type.
|
||||
* @param types - The base {@linkcode PokemonType}s to set; will populate `addedType` if above 3
|
||||
* @param multi - The expected {@linkcode TypeDamageMultiplier}
|
||||
*/
|
||||
it("should deal 2x dmg against soaked wonder guard target", async () => {
|
||||
game.override
|
||||
.enemySpecies(SpeciesId.SHEDINJA)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.starterSpecies(SpeciesId.MAGIKARP)
|
||||
.moveset([MoveId.SOAK, MoveId.FREEZE_DRY]);
|
||||
await game.classicMode.startBattle();
|
||||
function expectEffectiveness(types: typesArray, multi: TypeDamageMultiplier): void {
|
||||
enemy.summonData.types = types.slice(0, 2);
|
||||
if (types[2] !== undefined) {
|
||||
enemy.summonData.addedType = types[2];
|
||||
}
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
const moveType = feebas.getMoveType(allMoves[MoveId.FREEZE_DRY]);
|
||||
const eff = enemy.getAttackTypeEffectiveness(moveType, { source: feebas, move: allMoves[MoveId.FREEZE_DRY] });
|
||||
expect(
|
||||
eff,
|
||||
`Freeze-dry effectiveness against ${stringifyEnumArray(PokemonType, types)} was ${eff} instead of ${multi}!`,
|
||||
).toBe(multi);
|
||||
}
|
||||
|
||||
game.move.select(MoveId.SOAK);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
describe("Normal -", () => {
|
||||
it.each<{ name: string; types: typesArray; eff: TypeDamageMultiplier }>([
|
||||
{ name: "Pure Water", types: [PokemonType.WATER], eff: 2 },
|
||||
{ name: "Water/Ground", types: [PokemonType.WATER, PokemonType.GROUND], eff: 4 },
|
||||
{ name: "Water/Flying/Grass", types: [PokemonType.WATER, PokemonType.FLYING, PokemonType.GRASS], eff: 8 },
|
||||
{ name: "Water/Fire", types: [PokemonType.WATER, PokemonType.FIRE], eff: 1 },
|
||||
])("should be $effx effective against a $name-type opponent", ({ types, eff }) => {
|
||||
expectEffectiveness(types, eff);
|
||||
});
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
it("should deal 2x dmg against soaked wonder guard target", async () => {
|
||||
game.field.mockAbility(enemy, AbilityId.WONDER_GUARD);
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
expectEffectiveness([PokemonType.WATER], 2);
|
||||
});
|
||||
|
||||
it("should consider the target's Tera Type", async () => {
|
||||
// Steel type terastallized into Water; 2x
|
||||
enemy.teraType = PokemonType.WATER;
|
||||
enemy.isTerastallized = true;
|
||||
|
||||
expectEffectiveness([PokemonType.STEEL], 2);
|
||||
|
||||
// Water type terastallized into steel; 0.5x
|
||||
enemy.teraType = PokemonType.STEEL;
|
||||
expectEffectiveness([PokemonType.WATER], 2);
|
||||
});
|
||||
|
||||
it.each<{ name: string; types: typesArray; eff: TypeDamageMultiplier }>([
|
||||
{ name: "Pure Water", types: [PokemonType.WATER], eff: 2 },
|
||||
{ name: "Water/Ghost", types: [PokemonType.WATER, PokemonType.GHOST], eff: 0 },
|
||||
])("should be $effx effective against a $name-type opponent with Normalize", ({ types, eff }) => {
|
||||
game.field.mockAbility(feebas, AbilityId.NORMALIZE);
|
||||
expectEffectiveness(types, eff);
|
||||
});
|
||||
|
||||
it("should not stack with Electrify", async () => {
|
||||
feebas.addTag(BattlerTagType.ELECTRIFIED);
|
||||
expect(feebas.getMoveType(allMoves[MoveId.FREEZE_DRY])).toBe(PokemonType.ELECTRIC);
|
||||
|
||||
expectEffectiveness([PokemonType.WATER], 2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => {
|
||||
game.override.enemySpecies(SpeciesId.QUAGSIRE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FORESTS_CURSE);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to steel type terastallized into water", async () => {
|
||||
game.override.enemySpecies(SpeciesId.SKARMORY);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
enemy.teraType = PokemonType.WATER;
|
||||
enemy.isTerastallized = true;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 0.5x damage to water type terastallized into fire", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PELIPPER);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
enemy.teraType = PokemonType.FIRE;
|
||||
enemy.isTerastallized = true;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
|
||||
});
|
||||
|
||||
it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => {
|
||||
game.override.enemySpecies(SpeciesId.TERAPAGOS).enemyAbility(AbilityId.TERA_SHELL);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.SOAK);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to water type under Normalize", async () => {
|
||||
game.override.ability(AbilityId.NORMALIZE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 0.25x damage to rock/steel type under Normalize", async () => {
|
||||
game.override.ability(AbilityId.NORMALIZE).enemySpecies(SpeciesId.SHIELDON);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25);
|
||||
});
|
||||
|
||||
it("should deal 0x damage to water/ghost type under Normalize", async () => {
|
||||
game.override.ability(AbilityId.NORMALIZE).enemySpecies(SpeciesId.JELLICENT);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to water type under Electrify", async () => {
|
||||
game.override.enemyMoveset([MoveId.ELECTRIFY]);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 4x damage to water/flying type under Electrify", async () => {
|
||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.GYARADOS);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4);
|
||||
});
|
||||
|
||||
it("should deal 0x damage to water/ground type under Electrify", async () => {
|
||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.BARBOACH);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0);
|
||||
});
|
||||
|
||||
it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => {
|
||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.FLAPPLE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type during inverse battle", async () => {
|
||||
game.override.moveset([MoveId.FREEZE_DRY]).enemySpecies(SpeciesId.MAGIKARP);
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type during inverse battle under Normalize", async () => {
|
||||
game.override.moveset([MoveId.FREEZE_DRY]).ability(AbilityId.NORMALIZE).enemySpecies(SpeciesId.MAGIKARP);
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type during inverse battle under Electrify", async () => {
|
||||
game.override.moveset([MoveId.FREEZE_DRY]).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset([MoveId.ELECTRIFY]);
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => {
|
||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.GYARADOS);
|
||||
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(MoveId.FREEZE_DRY);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1);
|
||||
describe("Inverse Battle -", () => {
|
||||
beforeAll(() => {
|
||||
game.challengeMode.overrideGameWithChallenges(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type", async () => {
|
||||
expectEffectiveness([PokemonType.WATER], 2);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type under Normalize", async () => {
|
||||
game.field.mockAbility(feebas, AbilityId.NORMALIZE);
|
||||
expectEffectiveness([PokemonType.WATER], 2);
|
||||
});
|
||||
|
||||
it("should still deal 2x damage to Water type under Electrify", async () => {
|
||||
feebas.addTag(BattlerTagType.ELECTRIFIED);
|
||||
expectEffectiveness([PokemonType.WATER], 2);
|
||||
});
|
||||
|
||||
it("should deal 1x damage to Water/Flying type under Electrify", async () => {
|
||||
feebas.addTag(BattlerTagType.ELECTRIFIED);
|
||||
expectEffectiveness([PokemonType.WATER, PokemonType.FLYING], 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -32,16 +32,25 @@ describe("Moves - Synchronoise", () => {
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should consider the user's tera type if it is terastallized", async () => {
|
||||
// TODO: Write test
|
||||
it.todo("should affect all opponents that share a type with the user");
|
||||
|
||||
it("should consider the user's Tera Type if it is Terastallized", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BIDOOF]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
// force the player to be terastallized
|
||||
playerPokemon.teraType = PokemonType.WATER;
|
||||
playerPokemon.isTerastallized = true;
|
||||
game.move.select(MoveId.SYNCHRONOISE);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
game.move.selectWithTera(MoveId.SYNCHRONOISE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemyPokemon).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
// TODO: Write test
|
||||
it.todo("should fail if no opponents share a type with the user");
|
||||
|
||||
// TODO: Write test
|
||||
it.todo("should fail if the user is typeless");
|
||||
});
|
||||
|
@ -12,6 +12,8 @@ import { generateStarter } from "#test/test-utils/game-manager-utils";
|
||||
import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper";
|
||||
import { copyChallenge } from "data/challenge";
|
||||
|
||||
type challengeStub = { id: Challenges; value: number; severity: number };
|
||||
|
||||
/**
|
||||
* Helper to handle Challenge mode specifics
|
||||
*/
|
||||
@ -33,8 +35,9 @@ export class ChallengeModeHelper extends GameManagerHelper {
|
||||
* Runs the Challenge game to the summon phase.
|
||||
* @param gameMode - Optional game mode to set.
|
||||
* @returns A promise that resolves when the summon phase is reached.
|
||||
* @todo this duplicates nearly all its code with the classic mode variant...
|
||||
*/
|
||||
async runToSummon(species?: SpeciesId[]) {
|
||||
private async runToSummon(species?: SpeciesId[]) {
|
||||
await this.game.runToTitle();
|
||||
|
||||
if (this.game.override.disableShinies) {
|
||||
@ -88,4 +91,26 @@ export class ChallengeModeHelper extends GameManagerHelper {
|
||||
await this.game.phaseInterceptor.to(CommandPhase);
|
||||
console.log("==================[New Turn]==================");
|
||||
}
|
||||
|
||||
/**
|
||||
* Override an already-started game with the given challenges.
|
||||
* @param id - The challenge id
|
||||
* @param value - The challenge value
|
||||
* @param severity - The challenge severity
|
||||
* @todo Make severity optional for challenges that do not require it
|
||||
*/
|
||||
public overrideGameWithChallenges(id: Challenges, value: number, severity: number): void;
|
||||
/**
|
||||
* Override an already-started game with the given challenges.
|
||||
* @param challenges - One or more challenges to set.
|
||||
*/
|
||||
public overrideGameWithChallenges(challenges: challengeStub[]): void;
|
||||
public overrideGameWithChallenges(challenges: challengeStub[] | Challenges, value?: number, severity?: number): void {
|
||||
if (typeof challenges !== "object") {
|
||||
challenges = [{ id: challenges, value: value!, severity: severity! }];
|
||||
}
|
||||
for (const challenge of challenges) {
|
||||
this.game.scene.gameMode.setChallengeValue(challenge.id, challenge.value, challenge.severity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export class DailyModeHelper extends GameManagerHelper {
|
||||
* @returns A promise that resolves when the summon phase is reached.
|
||||
* @remarks Please do not use for starting normal battles - use {@linkcode startBattle} instead
|
||||
*/
|
||||
async runToSummon(): Promise<void> {
|
||||
private async runToSummon(): Promise<void> {
|
||||
await this.game.runToTitle();
|
||||
|
||||
if (this.game.override.disableShinies) {
|
||||
|
Loading…
Reference in New Issue
Block a user