Made getAttackTypeEffectiveness take an object for parameters; added FP tests

This commit is contained in:
Bertie690 2025-08-08 22:08:15 -04:00
parent 1f50ebdae0
commit f5e0ddd7af
8 changed files with 302 additions and 149 deletions

View File

@ -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)

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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);
}); });

View File

@ -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 () => {

View 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);
});
});
});