Moved inverse battle check to getTypeDamageMultiplier to avoid duplication; fixed tests

This commit is contained in:
Bertie690 2025-08-19 15:00:45 -04:00
parent 41d2b14cbd
commit 7b912ee033
8 changed files with 71 additions and 43 deletions

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
export enum PokemonType {
/** Typeless */
UNKNOWN = -1,
NORMAL = 0,
FIGHTING,

View File

@ -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<T extends BattlerTag>(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<T extends BattlerTag>(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));
}

View File

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

View File

@ -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 }>([

View File

@ -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<void>;
/**
* 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<void>;
async startBattle(species?: SpeciesId[]) {
await this.runToSummon(species);