From 7b912ee0336fe6f0d9c3f417cdd3ed7071a325c4 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 19 Aug 2025 15:00:45 -0400 Subject: [PATCH] Moved inverse battle check to `getTypeDamageMultiplier` to avoid duplication; fixed tests --- src/data/abilities/ability.ts | 6 ++-- src/data/moves/move.ts | 36 ++++++++++--------- src/data/type.ts | 20 ++++++++--- src/enums/pokemon-type.ts | 1 + src/field/pokemon.ts | 33 +++++++++-------- test/battle/inverse-battle.test.ts | 1 - test/moves/freeze-dry.test.ts | 2 +- .../helpers/challenge-mode-helper.ts | 15 ++++++-- 8 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 2959e3b62b3..0be0d57d57f 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -4199,9 +4199,9 @@ function getWeatherCondition(...weatherTypes: WeatherType[]): AbAttrCondition { 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")) { + // ignore non-attacks + const move = movesetMove.getMove(); + if (!move.is("AttackMove")) { return false; } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f80f16f8693..681b8dc7a1d 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -5395,7 +5395,7 @@ export class FreezeDryAttr extends VariableMoveTypeChartAttr { // 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; + multiplier.value *= 2 / normalEff; return true; } } @@ -5411,7 +5411,6 @@ export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAtt return false; } multiplier.value = 1; - return true; } } @@ -8147,12 +8146,13 @@ export class UpperHandCondition extends MoveCondition { /** * Attribute used for Conversion 2, to convert the user's type to a random type that resists the target's last used move. - * Fails if the user already has ALL types that resist the target's last used move. + * ~~Fails~~ Does nothing if the user already has ALL types that resist the target's last used move. * Fails if the opponent has not used a move yet - * Fails if the type is unknown or stellar + * ~~Fails~~ Does nothing if the type is unknown or stellar * * TODO: * If a move has its type changed (e.g. {@linkcode MoveId.HIDDEN_POWER}), it will check the new type. + * Does not fail when it should */ export class ResistLastMoveTypeAttr extends MoveEffectAttr { constructor() { @@ -8182,8 +8182,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { if (moveData.type === PokemonType.STELLAR || moveData.type === PokemonType.UNKNOWN) { return false; } - const userTypes = user.getTypes(); - const validTypes = this.getTypeResistances(globalScene.gameMode, moveData.type).filter(t => !userTypes.includes(t)); // valid types are ones that are not already the user's types + const validTypes = this.getTypeResistances(user, moveData.type) if (!validTypes.length) { return false; } @@ -8197,21 +8196,26 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { /** * Retrieve the types resisting a given type. Used by Conversion 2 - * @returns An array populated with Types, or an empty array if no resistances exist (Unknown or Stellar type) + * @param moveType - The type of the move having been used + * @returns An array containing all types that resist the given move's type + * and are not currently shared by the user */ - getTypeResistances(gameMode: GameMode, type: number): PokemonType[] { - const typeResistances: PokemonType[] = []; + private getTypeResistances(user: Pokemon, moveType: PokemonType): PokemonType[] { + const resistances: PokemonType[] = []; + const userTypes = user.getTypes(true, true) - for (let i = 0; i < Object.keys(PokemonType).length; i++) { - const multiplier = new NumberHolder(1); - multiplier.value = getTypeDamageMultiplier(type, i); - applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier); - if (multiplier.value < 1) { - typeResistances.push(i); + + for (const type of getEnumValues(PokemonType)) { + if (userTypes.includes(type)) { + continue; + } + const multiplier = getTypeDamageMultiplier(moveType, type); + if (multiplier < 1) { + resistances.push(type); } } - return typeResistances; + return resistances; } getCondition(): MoveConditionFunc { diff --git a/src/data/type.ts b/src/data/type.ts index 958a13df7b0..8dd51a887b0 100644 --- a/src/data/type.ts +++ b/src/data/type.ts @@ -1,15 +1,28 @@ +import { ChallengeType } from "#enums/challenge-type"; import { PokemonType } from "#enums/pokemon-type"; +import { applyChallenges } from "#utils/challenge-utils"; +import { NumberHolder } from "#utils/common"; export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8; +export type SingleTypeDamageMultiplier = 0 | 0.5 | 1 | 2; + /** - * Get the type effectiveness multiplier of one PokemonType against another. + * Get the base type effectiveness of one `PokemonType` against another. \ + * Accounts for Inverse Battle's reversed type effectiveness, but does not apply any other effects. * @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): SingleTypeDamageMultiplier { + const multi = new NumberHolder(getTypeChartMultiplier(attackType, defType)); + applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multi); + return multi.value as SingleTypeDamageMultiplier; +} + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: This simulates the Pokemon type chart with nested `switch case`s +function getTypeChartMultiplier(attackType: PokemonType, defType: PokemonType): SingleTypeDamageMultiplier { if (attackType === PokemonType.UNKNOWN || defType === PokemonType.UNKNOWN) { return 1; } @@ -270,10 +283,7 @@ export function getTypeDamageMultiplier(attackType: PokemonType, defType: Pokemo case PokemonType.STELLAR: return 1; } - - return 1; } - /** * Retrieve the color corresponding to a specific damage multiplier * @returns A color or undefined if the default color should be used diff --git a/src/enums/pokemon-type.ts b/src/enums/pokemon-type.ts index eca02bae275..08ccd6772d4 100644 --- a/src/enums/pokemon-type.ts +++ b/src/enums/pokemon-type.ts @@ -1,4 +1,5 @@ export enum PokemonType { + /** Typeless */ UNKNOWN = -1, NORMAL = 0, FIGHTING, diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f8e942106da..693230ffc24 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2520,32 +2520,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.isTerastallized ? 2 : 1; } - const types = this.getTypes(true, true, undefined, useIllusion); + const types = this.getTypes(true, true, false, useIllusion); const arena = globalScene.arena; // Handle flying v ground type immunity without removing flying type so effective types are still effective // Related to https://github.com/pagefaultgames/pokerogue/issues/524 - if (moveType === PokemonType.GROUND && (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY))) { - const flyingIndex = types.indexOf(PokemonType.FLYING); - if (flyingIndex > -1) { - types.splice(flyingIndex, 1); - } + // TODO: Fix once gravity makes pokemon actually grounded + if ( + moveType === PokemonType.GROUND && + types.includes(PokemonType.FLYING) && + (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY)) + ) { + types.splice(types.indexOf(PokemonType.FLYING), 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 + const typeMulti = getTypeDamageMultiplier(moveType, defenderType); + // If the target is immune to the type in question, check for effects that would ignore said nullification // TODO: Review if the `isActive` check is needed anymore if ( source?.isActive(true) && - typeMulti.value === 0 && + typeMulti === 0 && this.checkIgnoreTypeImmunity({ source, simulated, moveType, defenderType }) ) { - typeMulti.value = 1; + continue; } - multi.value *= typeMulti.value; + multi.value *= typeMulti; } // Apply any typing changes from Freeze-Dry, etc. @@ -2554,14 +2555,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } // 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 + getTypeDamageMultiplier(moveType, PokemonType.FLYING) === 2 ) { multi.value /= 2; if (!simulated) { @@ -4326,10 +4325,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { : this.summonData.tags.find(t => t.tagType === tagType); } + findTag(tagFilter: (tag: BattlerTag) => tag is T): T | undefined; + findTag(tagFilter: (tag: BattlerTag) => boolean): BattlerTag | undefined; findTag(tagFilter: (tag: BattlerTag) => boolean) { return this.summonData.tags.find(t => tagFilter(t)); } + findTags(tagFilter: (tag: BattlerTag) => tag is T): T[]; + findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[]; findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { return this.summonData.tags.filter(t => tagFilter(t)); } diff --git a/test/battle/inverse-battle.test.ts b/test/battle/inverse-battle.test.ts index 0b16063886b..f34c2f14135 100644 --- a/test/battle/inverse-battle.test.ts +++ b/test/battle/inverse-battle.test.ts @@ -149,7 +149,6 @@ 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); diff --git a/test/moves/freeze-dry.test.ts b/test/moves/freeze-dry.test.ts index 87ef0c5d210..d0acf578010 100644 --- a/test/moves/freeze-dry.test.ts +++ b/test/moves/freeze-dry.test.ts @@ -94,7 +94,7 @@ describe.sequential("Move - Freeze-Dry", () => { // Water type terastallized into steel; 0.5x enemy.teraType = PokemonType.STEEL; - expectEffectiveness([PokemonType.WATER], 2); + expectEffectiveness([PokemonType.WATER], 0.5); }); it.each<{ name: string; types: typesArray; eff: TypeDamageMultiplier }>([ diff --git a/test/test-utils/helpers/challenge-mode-helper.ts b/test/test-utils/helpers/challenge-mode-helper.ts index c9734c4550b..a7c91a7781a 100644 --- a/test/test-utils/helpers/challenge-mode-helper.ts +++ b/test/test-utils/helpers/challenge-mode-helper.ts @@ -70,10 +70,21 @@ export class ChallengeModeHelper extends GameManagerHelper { } /** - * Transitions to the start of a battle. - * @param species - Optional array of species to start the battle with. + * Transitions the challenge game to the start of a new battle. + * @param species - An array of {@linkcode Species} to summon. * @returns A promise that resolves when the battle is started. + * @todo This duplicates all its code with the classic mode variant... */ + async startBattle(species: SpeciesId[]): Promise; + /** + * Transitions the challenge game to the start of a new battle. + * Selects 3 daily run starters with a fixed seed of "test" + * (see `DailyRunConfig.getDailyRunStarters` in `daily-run.ts` for more info). + * @returns A promise that resolves when the battle is started. + * @deprecated - Specifying the starters helps prevent inconsistencies from internal RNG changes. + * @todo This duplicates all its code with the classic mode variant... + */ + async startBattle(): Promise; async startBattle(species?: SpeciesId[]) { await this.runToSummon(species);