diff --git a/src/data/ability.ts b/src/data/ability.ts index eec1240e1d3..c21b88832da 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -426,7 +426,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { } applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: PokemonMove, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type) < 2) { + if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, attacker) < 2) { cancelled.value = true; (args[0] as Utils.NumberHolder).value = 0; return true; @@ -1794,7 +1794,7 @@ function getAnticipationCondition(): AbAttrCondition { for (let opponent of pokemon.getOpponents()) { for (let move of opponent.moveset) { // move is super effective - if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type) >= 2) { + if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent) >= 2) { return true; } // move is a OHKO @@ -1816,7 +1816,7 @@ function getAnticipationCondition(): AbAttrCondition { Type.FIRE, Type.WATER, Type.GRASS, Type.ELECTRIC, Type.PSYCHIC, Type.ICE, Type.DRAGON, Type.DARK][iv_val]; - if (pokemon.getAttackTypeEffectiveness(type) >= 2) { + if (pokemon.getAttackTypeEffectiveness(type, opponent) >= 2) { return true; } } @@ -2472,6 +2472,25 @@ export class NoFusionAbilityAbAttr extends AbAttr { } } +export class IgnoreTypeImmunityAbAttr extends AbAttr { + defenderType: Type; + allowedMoveTypes: Type[]; + + constructor(defenderType: Type, allowedMoveTypes: Type[]) { + super(true); + this.defenderType = defenderType; + this.allowedMoveTypes = allowedMoveTypes; + } + + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (this.defenderType !== (args[1] as Type)) { + return false; + } + + return this.allowedMoveTypes.some(type => type === (args[0] as Type)); + } +} + function applyAbAttrsInternal(attrType: { new(...args: any[]): TAttr }, pokemon: Pokemon, applyFunc: AbAttrApplyFunc, args: any[], isAsync: boolean = false, showAbilityInstant: boolean = false, quiet: boolean = false, passive: boolean = false): Promise { return new Promise(resolve => { @@ -2973,15 +2992,15 @@ export function initAbilities() { .attr(IgnoreOpponentStatChangesAbAttr) .ignorable(), new Ability(Abilities.TINTED_LENS, 4) - .attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type) <= 0.5, 2), + .attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) <= 0.5, 2), new Ability(Abilities.FILTER, 4) - .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type) >= 2, 0.75) + .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75) .ignorable(), new Ability(Abilities.SLOW_START, 4) .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.SLOW_START, 5), - new Ability(Abilities.SCRAPPY, 4) - .attr(IntimidateImmunityAbAttr) - .partial(), + new Ability(Abilities.SCRAPPY, 4) + .attr(IgnoreTypeImmunityAbAttr, Type.GHOST, [Type.NORMAL, Type.FIGHTING]) + .attr(IntimidateImmunityAbAttr), new Ability(Abilities.STORM_DRAIN, 4) .attr(RedirectTypeMoveAbAttr, Type.WATER) .attr(TypeImmunityStatChangeAbAttr, Type.WATER, BattleStat.SPATK, 1) @@ -2990,7 +3009,7 @@ export function initAbilities() { .attr(BlockWeatherDamageAttr, WeatherType.HAIL) .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW), new Ability(Abilities.SOLID_ROCK, 4) - .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type) >= 2, 0.75) + .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75) .ignorable(), new Ability(Abilities.SNOW_WARNING, 4) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SNOW) @@ -3258,7 +3277,7 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr), new Ability(Abilities.DISGUISE, 7) - .attr(PreDefendMovePowerToOneAbAttr, (target, user, move) => target.formIndex == 0 && target.getAttackTypeEffectiveness(move.type) > 0) + .attr(PreDefendMovePowerToOneAbAttr, (target, user, move) => target.formIndex == 0 && target.getAttackTypeEffectiveness(move.type, user) > 0) .attr(PostSummonFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) .attr(PostBattleInitFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) .attr(PostDefendFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) @@ -3356,9 +3375,9 @@ export function initAbilities() { new Ability(Abilities.SHADOW_SHIELD, 7) .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getHpRatio() === 1, 0.5), new Ability(Abilities.PRISM_ARMOR, 7) - .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type) >= 2, 0.75), + .attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75), new Ability(Abilities.NEUROFORCE, 7) - .attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type) >= 2, 1.25), + .attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 1.25), new Ability(Abilities.INTREPID_SWORD, 8) .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, 1, true), new Ability(Abilities.DAUNTLESS_SHIELD, 8) @@ -3567,8 +3586,8 @@ export function initAbilities() { .attr(MoveAbilityBypassAbAttr, (pokemon, move: Move) => move.category === MoveCategory.STATUS) .partial(), new Ability(Abilities.MINDS_EYE, 9) - .ignorable() - .unimplemented(), + .attr(IgnoreTypeImmunityAbAttr, Type.GHOST, [Type.NORMAL, Type.FIGHTING]) + .ignorable(), // TODO: evasiveness bypass should not be ignored, but accuracy immunity should new Ability(Abilities.SUPERSWEET_SYRUP, 9) .unimplemented(), new Ability(Abilities.HOSPITALITY, 9) diff --git a/src/data/move.ts b/src/data/move.ts index 653ffc93c0a..afad81d4807 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -367,7 +367,7 @@ export class AttackMove extends Move { let attackScore = 0; - const effectiveness = target.getAttackTypeEffectiveness(this.type); + const effectiveness = target.getAttackTypeEffectiveness(this.type, user); attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; if (attackScore) { if (this.category === MoveCategory.PHYSICAL) { @@ -738,7 +738,7 @@ export class SacrificialAttr extends MoveEffectAttr { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { if (user.isBoss()) return -20; - return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type) - 0.5)); + return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5)); } } @@ -776,7 +776,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { if (user.isBoss()) return -10; - return Math.ceil(((1 - user.getHpRatio()/2) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type) - 0.5)); + return Math.ceil(((1 - user.getHpRatio()/2) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5)); } } @@ -2476,7 +2476,7 @@ export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultipli export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; - multiplier.value *= target.getAttackTypeEffectiveness(Type.FLYING); + multiplier.value *= target.getAttackTypeEffectiveness(Type.FLYING, user); return true; } } @@ -6453,9 +6453,9 @@ export function initMoves() { new AttackMove(Moves.RUINATION, Type.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9) .attr(TargetHalfHpDamageAttr), new AttackMove(Moves.COLLISION_COURSE, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type) >= 2 ? 5461/4096 : 1), + .attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461/4096 : 1), new AttackMove(Moves.ELECTRO_DRIFT, Type.ELECTRIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 9) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type) >= 2 ? 5461/4096 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461/4096 : 1) .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) .unimplemented(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1237f7bb55f..36dc4265efc 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -27,7 +27,7 @@ import { TempBattleStat } from '../data/temp-battle-stat'; import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from '../data/arena-tag'; import { ArenaTagType } from "../data/enums/arena-tag-type"; import { Biome } from "../data/enums/biome"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, FieldVariableMovePowerAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, VariableMoveTypeAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr } from '../data/ability'; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, FieldVariableMovePowerAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, VariableMoveTypeAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr } from '../data/ability'; import { Abilities } from "#app/data/enums/abilities"; import PokemonData from '../system/pokemon-data'; import Battle, { BattlerIndex } from '../battle'; @@ -880,7 +880,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getAttackMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier { const typeless = !!move.getMove().getAttrs(TypelessAttr).length; - const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move.getMove().type)); + const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move.getMove().type, source)); const cancelled = new Utils.BooleanHolder(false); if (!typeless) applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true); @@ -889,11 +889,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return (!cancelled.value ? typeMultiplier.value : 0) as TypeDamageMultiplier; } - getAttackTypeEffectiveness(moveType: Type): TypeDamageMultiplier { + getAttackTypeEffectiveness(moveType: Type, source?: Pokemon): TypeDamageMultiplier { if (moveType === Type.STELLAR) return this.isTerastallized() ? 2 : 1; const types = this.getTypes(true, true); - let multiplier = getTypeDamageMultiplier(moveType, types[0]) * (types.length > 1 ? getTypeDamageMultiplier(moveType, types[1]) : 1) * (types.length > 2 ? getTypeDamageMultiplier(moveType, types[2]) : 1) as TypeDamageMultiplier; + + const ignorableImmunities = source?.getAbility()?.getAttrs(IgnoreTypeImmunityAbAttr) || []; + const cancelled = new Utils.BooleanHolder(false); + + let multiplier = types.map(defType => + ignorableImmunities.some(attr => attr.apply(source, false, cancelled, [moveType, defType])) + ? 1 + : getTypeDamageMultiplier(moveType, defType) + ).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier; + // Handle strong winds lowering effectiveness of types super effective against pure flying if (this.scene.arena.weather?.weatherType === WeatherType.STRONG_WINDS && !this.scene.arena.weather.isEffectSuppressed(this.scene) && multiplier >= 2 && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) multiplier /= 2; @@ -904,12 +913,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const types = this.getTypes(true); const enemyTypes = pokemon.getTypes(true, true); const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, pokemon) : this.getStat(Stat.SPD)) <= pokemon.getBattleStat(Stat.SPD, this); - let atkScore = pokemon.getAttackTypeEffectiveness(types[0]) * (outspeed ? 1.25 : 1); - let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0]), 0.25); + let atkScore = pokemon.getAttackTypeEffectiveness(types[0], this) * (outspeed ? 1.25 : 1); + let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], pokemon), 0.25); if (types.length > 1) - atkScore *= pokemon.getAttackTypeEffectiveness(types[1]); + atkScore *= pokemon.getAttackTypeEffectiveness(types[1], this); if (enemyTypes.length > 1) - defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1]), 0.25)); + defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], pokemon), 0.25)); let hpDiffRatio = this.getHpRatio() + (1 - pokemon.getHpRatio()); if (outspeed) hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1); @@ -1262,7 +1271,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const cancelled = new Utils.BooleanHolder(false); const typeless = !!move.getAttrs(TypelessAttr).length; const typeMultiplier = new Utils.NumberHolder(!typeless && (moveCategory !== MoveCategory.STATUS || move.getAttrs(StatusMoveTypeImmunityAttr).find(attr => types.includes((attr as StatusMoveTypeImmunityAttr).immuneType))) - ? this.getAttackTypeEffectiveness(type) + ? this.getAttackTypeEffectiveness(type, source) : 1); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); if (typeless)