From 1f50ebdae0d256c32a9962da1952378287a99f09 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 8 Aug 2025 19:19:00 -0400 Subject: [PATCH 1/7] Cleaned up flying press/etc moves; removed `VariableMoveTypeMultiplierAttr` --- src/data/abilities/ability.ts | 4 +- src/data/moves/move.ts | 131 ++++++++++++++++------------------ src/field/pokemon.ts | 1 - 3 files changed, 63 insertions(+), 73 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index f5fd9b19f72..9512c3feaad 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -730,9 +730,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) ); } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 067bd05c2ae..a9b044fc857 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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, @@ -5349,48 +5349,60 @@ 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 { +/** + * Attribute for moves which have a custom type chart interaction. + */ +export class VariableMoveTypeChartAttr extends MoveAttr { /** - * 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 + * @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. + * @returns `true` if application of the attribute succeeds */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const multiplier = args[0] as NumberHolder; - if (target.isOfType(PokemonType.ICE)) { - multiplier.value = 0; + apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[]]): boolean { + return false; + } +} + +/** + * 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: [NumberHolder, PokemonType[]]): boolean { + const [multiplier, types] = args; + + if (types.includes(PokemonType.WATER)) { + multiplier.value = Math.max(multiplier.value, 2); return true; } return false; } } -export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { +/** + * 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[]]): 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[]]): boolean { const multiplier = args[0] as NumberHolder; multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, user); return true; @@ -5398,39 +5410,19 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { } /** - * Attribute for moves which have a custom type chart interaction. + * Attribute used by {@linkcode MoveId.SHEER_COLD} to implement its Gen VII+ ice ineffectiveness. */ -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 - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { +export class IceNoEffectTypeAttr extends VariableMoveTypeChartAttr { + apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[]]): boolean { + const [multiplier, types] = args; + if (types.includes(PokemonType.ICE)) { + multiplier.value = 0; + return true; + } return false; } } -/** - * This class forces Freeze-Dry to be super effective against Water Type. - */ -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 { - return false; - } - } -} export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -5916,8 +5908,8 @@ export class ProtectAttr extends AddBattlerTagAttr { for (const turnMove of user.getLastXMoves(-1).slice()) { if ( // Quick & Wide guard increment the Protect counter without using it for fail chance - !(allMoves[turnMove.move].hasAttr("ProtectAttr") || - [MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) || + !(allMoves[turnMove.move].hasAttr("ProtectAttr") || + [MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) || turnMove.result !== MoveResult.SUCCESS ) { break; @@ -8146,7 +8138,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))) { @@ -8417,8 +8411,7 @@ const MoveAttrs = Object.freeze({ TeraStarstormTypeAttr, MatchUserTypeAttr, CombinedPledgeTypeAttr, - VariableMoveTypeMultiplierAttr, - NeutralDamageAgainstFlyingTypeMultiplierAttr, + NeutralDamageAgainstFlyingTypeAttr, IceNoEffectTypeAttr, FlyingTypeMultiplierAttr, VariableMoveTypeChartAttr, @@ -10454,7 +10447,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) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index fe85e92772c..d8860352c1b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2400,7 +2400,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { : 1, ); - applyMoveAttrs("VariableMoveTypeMultiplierAttr", source, this, move, typeMultiplier); if (this.getTypes(true, true).find(t => move.isTypeImmune(source, this, t))) { typeMultiplier.value = 0; } From f5e0ddd7af71eb54a9b3cb201c50d4507081e58d Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 8 Aug 2025 22:08:15 -0400 Subject: [PATCH 2/7] Made `getAttackTypeEffectiveness` take an object for parameters; added FP tests --- src/data/abilities/ability.ts | 96 +++++-------- src/data/arena-tag.ts | 2 +- src/data/moves/move.ts | 19 +-- src/data/type.ts | 7 + src/field/pokemon.ts | 183 ++++++++++++++++-------- test/abilities/illusion.test.ts | 27 ++-- test/arena/weather-strong-winds.test.ts | 8 +- test/moves/flying-press.test.ts | 109 ++++++++++++++ 8 files changed, 302 insertions(+), 149 deletions(-) create mode 100644 test/moves/flying-press.test.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 9512c3feaad..28949fba47c 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -4188,71 +4188,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 +7055,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) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 15c2cde1d58..9588234ae45 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -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; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index a9b044fc857..01308de0e2a 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -999,7 +999,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)); @@ -1795,7 +1795,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)); } } @@ -1833,7 +1833,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)); } } @@ -1875,7 +1875,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)); } } @@ -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. */ export class FlyingTypeMultiplierAttr extends VariableMoveTypeChartAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[]]): boolean { - const multiplier = args[0] as NumberHolder; - multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, user); + apply(user: Pokemon, target: Pokemon, _move: Move, args: [multiplier: NumberHolder, types: PokemonType[]]): boolean { + const multiplier = args[0]; + multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, {source: user}); return true; } } @@ -11383,9 +11383,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) diff --git a/src/data/type.ts b/src/data/type.ts index c9bf346fb85..958a13df7b0 100644 --- a/src/data/type.ts +++ b/src/data/type.ts @@ -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; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d8860352c1b..68e530610b2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -129,7 +129,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"; @@ -204,6 +205,38 @@ type getBaseDamageParams = Omit; /** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */ type getAttackDamageParams = Omit; +/** + * 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}, @@ -2396,7 +2429,7 @@ 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, ); @@ -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. - * @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; @@ -2491,57 +2529,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); + } + // 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; } /** @@ -2561,10 +2621,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; @@ -2578,7 +2643,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 diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index 2343a11cb74..c445cd1d610 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -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); }); diff --git a/test/arena/weather-strong-winds.test.ts b/test/arena/weather-strong-winds.test.ts index 1800027f59c..1060759bf05 100644 --- a/test/arena/weather-strong-winds.test.ts +++ b/test/arena/weather-strong-winds.test.ts @@ -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 () => { diff --git a/test/moves/flying-press.test.ts b/test/moves/flying-press.test.ts new file mode 100644 index 00000000000..ba1412e9099 --- /dev/null +++ b/test/moves/flying-press.test.ts @@ -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); + }); + }); +}); From 7c60d0a5b1aa95874985ed2147289abcf38c88bf Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 8 Aug 2025 22:15:06 -0400 Subject: [PATCH 3/7] Added extra Wonder Guard test --- test/moves/flying-press.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/moves/flying-press.test.ts b/test/moves/flying-press.test.ts index ba1412e9099..36f572b7a94 100644 --- a/test/moves/flying-press.test.ts +++ b/test/moves/flying-press.test.ts @@ -43,7 +43,6 @@ describe.sequential("Move - Flying Press", () => { // 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); @@ -106,4 +105,18 @@ describe.sequential("Move - Flying Press", () => { checkEffForAllTypes(PokemonType.NORMAL); }); }); + + it("should deal 2x to Wonder Guard Shedinja under Electrify", () => { + const enemy = game.field.getEnemyPokemon(); + game.field.mockAbility(enemy, AbilityId.WONDER_GUARD); + enemy.resetSummonData(); + + hawlucha.addTag(BattlerTagType.ELECTRIFIED); + + const flyingPressEff = enemy.getAttackTypeEffectiveness(hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]), { + source: hawlucha, + move: allMoves[MoveId.FLYING_PRESS], + }); + expect(flyingPressEff).toBe(2); + }); }); From 216018b4099a8b9d3b3fb98f23db9270f38d90ae Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 8 Aug 2025 22:27:56 -0400 Subject: [PATCH 4/7] Fixed bugs with freeze dry --- src/data/moves/move.ts | 28 +++++++++++++++------------- src/field/pokemon.ts | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 01308de0e2a..4f6a73f759f 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -5359,10 +5359,11 @@ export class VariableMoveTypeChartAttr extends MoveAttr { * @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. + * `[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: [multiplier: NumberHolder, types: PokemonType[]]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean { return false; } } @@ -5371,14 +5372,16 @@ export class VariableMoveTypeChartAttr extends MoveAttr { * 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: [NumberHolder, PokemonType[]]): boolean { - const [multiplier, types] = args; - - if (types.includes(PokemonType.WATER)) { - multiplier.value = Math.max(multiplier.value, 2); - return true; + 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; } - 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; } } @@ -5387,7 +5390,7 @@ export class FreezeDryAttr extends VariableMoveTypeChartAttr { * against all ungrounded flying types. */ export class NeutralDamageAgainstFlyingTypeAttr 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[], moveType: PokemonType]): boolean { const [multiplier, types] = args; if (target.isGrounded() || !types.includes(PokemonType.FLYING)) { return false; @@ -5402,7 +5405,7 @@ export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAtt * 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[]]): boolean { + 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; @@ -5413,7 +5416,7 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeChartAttr { * 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[]]): boolean { + 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; @@ -5423,7 +5426,6 @@ export class IceNoEffectTypeAttr extends VariableMoveTypeChartAttr { } } - export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const accuracy = args[0] as NumberHolder; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 68e530610b2..e5c4cb3a46a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2547,7 +2547,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // Apply any typing changes from Freeze-Dry, etc. if (move) { - applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multi, types); + applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multi, types, moveType); } // Handle strong winds lowering effectiveness of types super effective against pure flying From 13a4b99072dd8b2d8f304468dc341812036d50a5 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 9 Aug 2025 13:44:28 -0400 Subject: [PATCH 5/7] Added `overrideGameWithChallenges` --- src/game-mode.ts | 17 +- test/abilities/tera-shell.test.ts | 15 + test/battle/inverse-battle.test.ts | 31 +- test/moves/flying-press.test.ts | 38 +- test/moves/freeze-dry.test.ts | 398 +++++------------- test/moves/synchronoise.test.ts | 21 +- .../helpers/challenge-mode-helper.ts | 27 +- test/test-utils/helpers/daily-mode-helper.ts | 2 +- 8 files changed, 198 insertions(+), 351 deletions(-) diff --git a/src/game-mode.ts b/src/game-mode.ts index 82f7b4fa77f..f634a6418eb 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -61,15 +61,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; + } + }); } /** diff --git a/test/abilities/tera-shell.test.ts b/test/abilities/tera-shell.test.ts index 4183cd4d0a6..0fe532cdb4f 100644 --- a/test/abilities/tera-shell.test.ts +++ b/test/abilities/tera-shell.test.ts @@ -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); + }); }); diff --git a/test/battle/inverse-battle.test.ts b/test/battle/inverse-battle.test.ts index 66a21e80009..0b16063886b 100644 --- a/test/battle/inverse-battle.test.ts +++ b/test/battle/inverse-battle.test.ts @@ -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); diff --git a/test/moves/flying-press.test.ts b/test/moves/flying-press.test.ts index 36f572b7a94..96da0a7c051 100644 --- a/test/moves/flying-press.test.ts +++ b/test/moves/flying-press.test.ts @@ -5,7 +5,7 @@ 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 type { EnemyPokemon, PlayerPokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; import { getEnumValues } from "#utils/enums"; import { toTitleCase } from "#utils/strings"; @@ -16,6 +16,7 @@ describe.sequential("Move - Flying Press", () => { let phaserGame: Phaser.Game; let game: GameManager; let hawlucha: PlayerPokemon; + let enemy: EnemyPokemon; beforeAll(async () => { phaserGame = new Phaser.Game({ @@ -26,32 +27,29 @@ describe.sequential("Move - Flying Press", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH) - .startingLevel(100) - .enemyLevel(100); + .enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.HAWLUCHA]); + hawlucha = game.field.getPlayerPokemon(); + enemy = game.field.getEnemyPokemon(); }); afterAll(() => { game.phaseInterceptor.restoreOg(); }); - // Reset temporary summon data overrides to reset effects + // Reset temp data after each test afterEach(() => { hawlucha.resetSummonData(); - expect(hawlucha).not.toHaveBattlerTag(BattlerTagType.ELECTRIFIED); - expect(hawlucha.hasAbility(AbilityId.NORMALIZE)).toBe(false); + enemy.resetSummonData(); }); 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 }); @@ -71,7 +69,7 @@ describe.sequential("Move - Flying Press", () => { } } - describe("Normal", () => { + describe("Normal -", () => { it("should deal damage as a Fighting/Flying type move by default", async () => { checkEffForAllTypes(PokemonType.FIGHTING); }); @@ -85,12 +83,25 @@ describe.sequential("Move - Flying Press", () => { 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", () => { + describe("Inverse Battle -", () => { beforeAll(() => { - game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + game.challengeMode.overrideGameWithChallenges(Challenges.INVERSE_BATTLE, 1, 1); }); + it("should deal damage as a Fighting/Flying type move by default", async () => { checkEffForAllTypes(PokemonType.FIGHTING); }); @@ -107,10 +118,7 @@ describe.sequential("Move - Flying Press", () => { }); it("should deal 2x to Wonder Guard Shedinja under Electrify", () => { - const enemy = game.field.getEnemyPokemon(); game.field.mockAbility(enemy, AbilityId.WONDER_GUARD); - enemy.resetSummonData(); - hawlucha.addTag(BattlerTagType.ELECTRIFIED); const flyingPressEff = enemy.getAttackTypeEffectiveness(hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]), { diff --git a/test/moves/freeze-dry.test.ts b/test/moves/freeze-dry.test.ts index 0b22d4f0997..87ef0c5d210 100644 --- a/test/moves/freeze-dry.test.ts +++ b/test/moves/freeze-dry.test.ts @@ -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); + }); }); }); diff --git a/test/moves/synchronoise.test.ts b/test/moves/synchronoise.test.ts index 98178b66d00..4fb1a7ca161 100644 --- a/test/moves/synchronoise.test.ts +++ b/test/moves/synchronoise.test.ts @@ -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"); }); diff --git a/test/test-utils/helpers/challenge-mode-helper.ts b/test/test-utils/helpers/challenge-mode-helper.ts index 3952685a560..16b08c1389a 100644 --- a/test/test-utils/helpers/challenge-mode-helper.ts +++ b/test/test-utils/helpers/challenge-mode-helper.ts @@ -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); + } + } } diff --git a/test/test-utils/helpers/daily-mode-helper.ts b/test/test-utils/helpers/daily-mode-helper.ts index 7aa1e699118..a288b52e24f 100644 --- a/test/test-utils/helpers/daily-mode-helper.ts +++ b/test/test-utils/helpers/daily-mode-helper.ts @@ -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 { + private async runToSummon(): Promise { await this.game.runToTitle(); if (this.game.override.disableShinies) { From 371e99a4a2510eac4e139629dda1cc23604951c9 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 10 Aug 2025 14:18:00 -0400 Subject: [PATCH 6/7] Fix flying-press.test.ts --- test/moves/flying-press.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/moves/flying-press.test.ts b/test/moves/flying-press.test.ts index 96da0a7c051..fb198b5c0d3 100644 --- a/test/moves/flying-press.test.ts +++ b/test/moves/flying-press.test.ts @@ -115,16 +115,16 @@ describe.sequential("Move - Flying Press", () => { hawlucha.setTempAbility(allAbilities[AbilityId.NORMALIZE]); checkEffForAllTypes(PokemonType.NORMAL); }); - }); - it("should deal 2x to Wonder Guard Shedinja under Electrify", () => { - game.field.mockAbility(enemy, AbilityId.WONDER_GUARD); - hawlucha.addTag(BattlerTagType.ELECTRIFIED); + 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 flyingPressEff = enemy.getAttackTypeEffectiveness(hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]), { - source: hawlucha, - move: allMoves[MoveId.FLYING_PRESS], + 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); }); - expect(flyingPressEff).toBe(2); - }); }); From 7e402d02b0cc885a1b51a20e80663007084d3b0c Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 10 Aug 2025 14:20:20 -0400 Subject: [PATCH 7/7] Added missing brace --- test/moves/flying-press.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/moves/flying-press.test.ts b/test/moves/flying-press.test.ts index fb198b5c0d3..b0b75f8e927 100644 --- a/test/moves/flying-press.test.ts +++ b/test/moves/flying-press.test.ts @@ -127,4 +127,5 @@ describe.sequential("Move - Flying Press", () => { }); expect(flyingPressEff).toBe(0.125); }); + }); });