mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-16 12:29:26 +02:00
Made getAttackTypeEffectiveness
take an object for parameters; added FP tests
This commit is contained in:
parent
1f50ebdae0
commit
f5e0ddd7af
@ -4188,71 +4188,43 @@ function getWeatherCondition(...weatherTypes: WeatherType[]): AbAttrCondition {
|
|||||||
if (globalScene.arena.weather?.isEffectSuppressed()) {
|
if (globalScene.arena.weather?.isEffectSuppressed()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const weatherType = globalScene.arena.weather?.weatherType;
|
return weatherTypes.includes(globalScene.arena.getWeatherType());
|
||||||
return !!weatherType && weatherTypes.indexOf(weatherType) > -1;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAnticipationCondition(): AbAttrCondition {
|
/**
|
||||||
return (pokemon: Pokemon) => {
|
* Condition used by {@linkcode AbilityId.ANTICIPATION} to show a message if any opponent knows a
|
||||||
for (const opponent of pokemon.getOpponents()) {
|
* "dangerous" move.
|
||||||
for (const move of opponent.moveset) {
|
* @param pokemon - The {@linkcode Pokemon} with this ability
|
||||||
// ignore null/undefined moves
|
* @returns Whether the message should be shown
|
||||||
if (!move) {
|
*/
|
||||||
continue;
|
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;
|
||||||
}
|
}
|
||||||
// the move's base type (not accounting for variable type changes) is super effective
|
|
||||||
if (
|
if (move.hasAttr("OneHitKOAttr")) {
|
||||||
move.getMove().is("AttackMove") &&
|
|
||||||
pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// move is a OHKO
|
|
||||||
if (move.getMove().hasAttr("OneHitKOAttr")) {
|
// Check whether the move's base type (not accounting for variable type changes) is super effective
|
||||||
return true;
|
const type = new NumberHolder(
|
||||||
}
|
pokemon.getAttackTypeEffectiveness(move.type, {
|
||||||
// edge case for hidden power, type is computed
|
source: opponent,
|
||||||
if (move.getMove().id === MoveId.HIDDEN_POWER) {
|
ignoreStrongWinds: true,
|
||||||
const iv_val = Math.floor(
|
move: move,
|
||||||
(((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 = [
|
// edge case for hidden power, type is computed
|
||||||
PokemonType.FIGHTING,
|
applyMoveAttrs("HiddenPowerTypeAttr", opponent, pokemon, move, type);
|
||||||
PokemonType.FLYING,
|
return type.value >= 2;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an ability condition that causes the ability to fail if that ability
|
* Creates an ability condition that causes the ability to fail if that ability
|
||||||
@ -7083,7 +7055,7 @@ export function initAbilities() {
|
|||||||
.attr(PostFaintContactDamageAbAttr, 4)
|
.attr(PostFaintContactDamageAbAttr, 4)
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(AbilityId.ANTICIPATION, 4)
|
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)
|
new Ability(AbilityId.FOREWARN, 4)
|
||||||
.attr(ForewarnAbAttr),
|
.attr(ForewarnAbAttr),
|
||||||
new Ability(AbilityId.UNAWARE, 4)
|
new Ability(AbilityId.UNAWARE, 4)
|
||||||
|
@ -958,7 +958,7 @@ class StealthRockTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDamageHpRatio(pokemon: Pokemon): number {
|
getDamageHpRatio(pokemon: Pokemon): number {
|
||||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, { ignoreStrongWinds: true });
|
||||||
|
|
||||||
let damageHpRatio = 0;
|
let damageHpRatio = 0;
|
||||||
|
|
||||||
|
@ -999,7 +999,7 @@ export class AttackMove extends Move {
|
|||||||
const ret = super.getTargetBenefitScore(user, target, move);
|
const ret = super.getTargetBenefitScore(user, target, move);
|
||||||
let attackScore = 0;
|
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);
|
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 [ thisStat, offStat ]: EffectiveStat[] = this.category === MoveCategory.PHYSICAL ? [ Stat.ATK, Stat.SPATK ] : [ Stat.SPATK, Stat.ATK ];
|
||||||
const statHolder = new NumberHolder(user.getEffectiveStat(thisStat, target));
|
const statHolder = new NumberHolder(user.getEffectiveStat(thisStat, target));
|
||||||
@ -1795,7 +1795,7 @@ export class SacrificialAttr extends MoveEffectAttr {
|
|||||||
if (user.isBoss()) {
|
if (user.isBoss()) {
|
||||||
return -20;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1833,7 +1833,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
|
|||||||
if (user.isBoss()) {
|
if (user.isBoss()) {
|
||||||
return -20;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1875,7 +1875,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
|
|||||||
if (user.isBoss()) {
|
if (user.isBoss()) {
|
||||||
return -10;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5402,9 +5402,9 @@ export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAtt
|
|||||||
* Attribute used by {@linkcode MoveId.FLYING_PRESS} to add the Flying Type to its type effectiveness.
|
* Attribute used by {@linkcode MoveId.FLYING_PRESS} to add the Flying Type to its type effectiveness.
|
||||||
*/
|
*/
|
||||||
export class FlyingTypeMultiplierAttr extends VariableMoveTypeChartAttr {
|
export class FlyingTypeMultiplierAttr extends VariableMoveTypeChartAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[]]): boolean {
|
apply(user: Pokemon, target: Pokemon, _move: Move, args: [multiplier: NumberHolder, types: PokemonType[]]): boolean {
|
||||||
const multiplier = args[0] as NumberHolder;
|
const multiplier = args[0];
|
||||||
multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, user);
|
multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, {source: user});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11383,9 +11383,10 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.RUINATION, PokemonType.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9)
|
new AttackMove(MoveId.RUINATION, PokemonType.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9)
|
||||||
.attr(TargetHalfHpDamageAttr),
|
.attr(TargetHalfHpDamageAttr),
|
||||||
new AttackMove(MoveId.COLLISION_COURSE, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
|
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)
|
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(),
|
.makesContact(),
|
||||||
new SelfStatusMove(MoveId.SHED_TAIL, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
new SelfStatusMove(MoveId.SHED_TAIL, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
||||||
.attr(AddSubstituteAttr, 0.5, true)
|
.attr(AddSubstituteAttr, 0.5, true)
|
||||||
|
@ -2,6 +2,13 @@ import { PokemonType } from "#enums/pokemon-type";
|
|||||||
|
|
||||||
export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8;
|
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 {
|
export function getTypeDamageMultiplier(attackType: PokemonType, defType: PokemonType): TypeDamageMultiplier {
|
||||||
if (attackType === PokemonType.UNKNOWN || defType === PokemonType.UNKNOWN) {
|
if (attackType === PokemonType.UNKNOWN || defType === PokemonType.UNKNOWN) {
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -129,7 +129,8 @@ import {
|
|||||||
TempStatStageBoosterModifier,
|
TempStatStageBoosterModifier,
|
||||||
} from "#modifiers/modifier";
|
} from "#modifiers/modifier";
|
||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
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 { getMoveTargets } from "#moves/move-utils";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import { loadMoveAnimations } from "#sprites/pokemon-asset-loader";
|
import { loadMoveAnimations } from "#sprites/pokemon-asset-loader";
|
||||||
@ -204,6 +205,38 @@ type getBaseDamageParams = Omit<damageParams, "effectiveness">;
|
|||||||
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
|
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
|
||||||
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
|
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 {
|
export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
/**
|
/**
|
||||||
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
|
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
|
||||||
@ -2396,7 +2429,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
const typeMultiplier = new NumberHolder(
|
const typeMultiplier = new NumberHolder(
|
||||||
move.category !== MoveCategory.STATUS || move.hasAttr("RespectAttackTypeImmunityAttr")
|
move.category !== MoveCategory.STATUS || move.hasAttr("RespectAttackTypeImmunityAttr")
|
||||||
? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move, useIllusion)
|
? this.getAttackTypeEffectiveness(moveType, { source, simulated, move, useIllusion })
|
||||||
: 1,
|
: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2459,26 +2492,31 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the move's type effectiveness multiplier based on the target's type/s.
|
* Calculate the type effectiveness multiplier of a Move used **against** this Pokemon.
|
||||||
* @param moveType {@linkcode PokemonType} the type of the move being used
|
* @param moveType - The {@linkcode PokemonType} of the move being used
|
||||||
* @param source {@linkcode Pokemon} the Pokemon using the move
|
* @param source - The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities
|
||||||
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
* and the effects of Foresight/Odor Sleuth
|
||||||
* @param simulated tag to only apply the strong winds effect message when the move is used
|
* @param ignoreStrongWinds - If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks);
|
||||||
* @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
|
* default `false`
|
||||||
* @param useIllusion - Whether we want the attack type effectiveness on the illusion or not
|
* @param simulated - If `true`, will prevent changes to game state during calculations; default `false`
|
||||||
* @returns a multiplier for the type effectiveness
|
* @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(
|
getAttackTypeEffectiveness(
|
||||||
moveType: PokemonType,
|
moveType: PokemonType,
|
||||||
source?: Pokemon,
|
{
|
||||||
|
source,
|
||||||
ignoreStrongWinds = false,
|
ignoreStrongWinds = false,
|
||||||
simulated = true,
|
simulated = true,
|
||||||
move?: Move,
|
move,
|
||||||
useIllusion = false,
|
useIllusion = false,
|
||||||
|
}: getAttackTypeEffectivenessParams = {},
|
||||||
): TypeDamageMultiplier {
|
): TypeDamageMultiplier {
|
||||||
if (moveType === PokemonType.STELLAR) {
|
if (moveType === PokemonType.STELLAR) {
|
||||||
return this.isTerastallized ? 2 : 1;
|
return this.isTerastallized ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const types = this.getTypes(true, true, undefined, useIllusion);
|
const types = this.getTypes(true, true, undefined, useIllusion);
|
||||||
const arena = globalScene.arena;
|
const arena = globalScene.arena;
|
||||||
|
|
||||||
@ -2491,16 +2529,71 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let multiplier = types
|
const multi = new NumberHolder(1);
|
||||||
.map(defenderType => {
|
for (const defenderType of types) {
|
||||||
const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
const typeMulti = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
||||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, typeMulti);
|
||||||
if (move) {
|
// If the target is immune to the type in question, check for any effects that would ignore said effect
|
||||||
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defenderType);
|
// 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;
|
||||||
}
|
}
|
||||||
if (source) {
|
multi.value *= typeMulti.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply any typing changes from Freeze-Dry, etc.
|
||||||
|
if (move) {
|
||||||
|
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multi, types);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
if (
|
||||||
|
!ignoreStrongWinds &&
|
||||||
|
arena.getWeatherType() === WeatherType.STRONG_WINDS &&
|
||||||
|
!arena.weather?.isEffectSuppressed() &&
|
||||||
|
types.includes(PokemonType.FLYING) &&
|
||||||
|
typeMultiplierAgainstFlying.value === 2
|
||||||
|
) {
|
||||||
|
multi.value /= 2;
|
||||||
|
if (!simulated) {
|
||||||
|
globalScene.phaseManager.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
const ignoreImmunity = new BooleanHolder(false);
|
||||||
if (source.isActive(true) && source.hasAbilityWithAttr("IgnoreTypeImmunityAbAttr")) {
|
|
||||||
applyAbAttrs("IgnoreTypeImmunityAbAttr", {
|
applyAbAttrs("IgnoreTypeImmunityAbAttr", {
|
||||||
pokemon: source,
|
pokemon: source,
|
||||||
cancelled: ignoreImmunity,
|
cancelled: ignoreImmunity,
|
||||||
@ -2508,40 +2601,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
moveType,
|
moveType,
|
||||||
defenderType,
|
defenderType,
|
||||||
});
|
});
|
||||||
}
|
return ignoreImmunity.value;
|
||||||
if (ignoreImmunity.value) {
|
|
||||||
if (multiplier.value === 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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) &&
|
|
||||||
typeMultiplierAgainstFlying.value === 2
|
|
||||||
) {
|
|
||||||
multiplier /= 2;
|
|
||||||
if (!simulated) {
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return multiplier as TypeDamageMultiplier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2561,10 +2621,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* Based on how effectively this Pokemon defends against the opponent's types.
|
* Based on how effectively this Pokemon defends against the opponent's types.
|
||||||
* This score cannot be higher than 4.
|
* 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) {
|
if (enemyTypes.length > 1) {
|
||||||
defScore *=
|
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;
|
const moveset = this.moveset;
|
||||||
@ -2578,7 +2643,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const moveType = resolvedMove.type;
|
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.
|
// Add STAB multiplier for attack type effectiveness.
|
||||||
// For now, simply don't apply STAB to moves that may change type
|
// For now, simply don't apply STAB to moves that may change type
|
||||||
|
@ -88,6 +88,7 @@ describe("Abilities - Illusion", () => {
|
|||||||
expect(game.field.getPlayerPokemon().summonData.illusion).toBeFalsy();
|
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 () => {
|
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]);
|
game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]);
|
||||||
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]);
|
||||||
@ -97,22 +98,16 @@ describe("Abilities - Illusion", () => {
|
|||||||
|
|
||||||
const flameThrower = enemy.getMoveset()[0]!.getMove();
|
const flameThrower = enemy.getMoveset()[0]!.getMove();
|
||||||
const psychic = enemy.getMoveset()[1]!.getMove();
|
const psychic = enemy.getMoveset()[1]!.getMove();
|
||||||
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(
|
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(flameThrower.type, {
|
||||||
flameThrower.type,
|
source: enemy,
|
||||||
enemy,
|
move: flameThrower,
|
||||||
undefined,
|
useIllusion: true,
|
||||||
undefined,
|
});
|
||||||
flameThrower,
|
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(psychic.type, {
|
||||||
true,
|
source: enemy,
|
||||||
);
|
move: psychic,
|
||||||
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(
|
useIllusion: true,
|
||||||
psychic.type,
|
});
|
||||||
enemy,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
psychic,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
|
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ describe("Weather - Strong Winds", () => {
|
|||||||
game.move.select(MoveId.THUNDERBOLT);
|
game.move.select(MoveId.THUNDERBOLT);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
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 () => {
|
it("electric type move is neutral for flying type pokemon", async () => {
|
||||||
@ -53,7 +53,7 @@ describe("Weather - Strong Winds", () => {
|
|||||||
game.move.select(MoveId.THUNDERBOLT);
|
game.move.select(MoveId.THUNDERBOLT);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
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 () => {
|
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);
|
game.move.select(MoveId.ICE_BEAM);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
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 () => {
|
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);
|
game.move.select(MoveId.ROCK_SLIDE);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
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 () => {
|
it("weather goes away when last trainer pokemon dies to indirect damage", async () => {
|
||||||
|
109
test/moves/flying-press.test.ts
Normal file
109
test/moves/flying-press.test.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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 { 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;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
|
.battleStyle("single")
|
||||||
|
.criticalHits(false)
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.SPLASH)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(100);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.HAWLUCHA]);
|
||||||
|
hawlucha = game.field.getPlayerPokemon();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset temporary summon data overrides to reset effects
|
||||||
|
afterEach(() => {
|
||||||
|
console.log("Apple");
|
||||||
|
hawlucha.resetSummonData();
|
||||||
|
expect(hawlucha).not.toHaveBattlerTag(BattlerTagType.ELECTRIFIED);
|
||||||
|
expect(hawlucha.hasAbility(AbilityId.NORMALIZE)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pokemonTypes = getEnumValues(PokemonType);
|
||||||
|
|
||||||
|
function checkEffForAllTypes(primaryType: PokemonType) {
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Inverse", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
game.challengeMode.addChallenge(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user