mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-21 07:42:25 +02:00
Merge branch 'beta' into damo-balance-1
This commit is contained in:
commit
e47234f3c6
@ -12,7 +12,7 @@ import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffect
|
|||||||
import { Gender } from "./gender";
|
import { Gender } from "./gender";
|
||||||
import type Move from "./move";
|
import type Move from "./move";
|
||||||
import { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
import { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
||||||
import type { ArenaTrapTag } from "./arena-tag";
|
import type { ArenaTrapTag, SuppressAbilitiesTag } from "./arena-tag";
|
||||||
import { ArenaTagSide } from "./arena-tag";
|
import { ArenaTagSide } from "./arena-tag";
|
||||||
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||||
import { TerrainType } from "./terrain";
|
import { TerrainType } from "./terrain";
|
||||||
@ -2197,6 +2197,34 @@ export class PostSummonRemoveArenaTagAbAttr extends PostSummonAbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic class to add an arena tag upon switching in
|
||||||
|
*/
|
||||||
|
export class PostSummonAddArenaTagAbAttr extends PostSummonAbAttr {
|
||||||
|
private readonly tagType: ArenaTagType;
|
||||||
|
private readonly turnCount: number;
|
||||||
|
private readonly side?: ArenaTagSide;
|
||||||
|
private readonly quiet?: boolean;
|
||||||
|
private sourceId: number;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(tagType: ArenaTagType, turnCount: number, side?: ArenaTagSide, quiet?: boolean) {
|
||||||
|
super(false);
|
||||||
|
this.tagType = tagType;
|
||||||
|
this.turnCount = turnCount;
|
||||||
|
this.side = side;
|
||||||
|
this.quiet = quiet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
|
||||||
|
this.sourceId = pokemon.id;
|
||||||
|
if (!simulated) {
|
||||||
|
globalScene.arena.addTag(this.tagType, this.turnCount, undefined, this.sourceId, this.side, this.quiet);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PostSummonMessageAbAttr extends PostSummonAbAttr {
|
export class PostSummonMessageAbAttr extends PostSummonAbAttr {
|
||||||
private messageFunc: (pokemon: Pokemon) => string;
|
private messageFunc: (pokemon: Pokemon) => string;
|
||||||
|
|
||||||
@ -2941,6 +2969,26 @@ export class PreLeaveFieldClearWeatherAbAttr extends PreLeaveFieldAbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the active {@linkcode SuppressAbilitiesTag} when a pokemon with {@linkcode Abilities.NEUTRALIZING_GAS} leaves the field
|
||||||
|
*/
|
||||||
|
export class PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr extends PreLeaveFieldAbAttr {
|
||||||
|
constructor() {
|
||||||
|
super(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override applyPreLeaveField(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
|
||||||
|
if (!simulated) {
|
||||||
|
const suppressTag = globalScene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag;
|
||||||
|
if (suppressTag) {
|
||||||
|
suppressTag.onSourceLeave(globalScene.arena);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return simulated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PreStatStageChangeAbAttr extends AbAttr {
|
export class PreStatStageChangeAbAttr extends AbAttr {
|
||||||
applyPreStatStageChange(
|
applyPreStatStageChange(
|
||||||
pokemon: Pokemon | null,
|
pokemon: Pokemon | null,
|
||||||
@ -4692,21 +4740,6 @@ export class MoveAbilityBypassAbAttr extends AbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SuppressFieldAbilitiesAbAttr extends AbAttr {
|
|
||||||
constructor() {
|
|
||||||
super(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
|
||||||
const ability = (args[0] as Ability);
|
|
||||||
if (!ability.hasAttr(UnsuppressableAbilityAbAttr) && !ability.hasAttr(SuppressFieldAbilitiesAbAttr)) {
|
|
||||||
cancelled.value = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AlwaysHitAbAttr extends AbAttr { }
|
export class AlwaysHitAbAttr extends AbAttr { }
|
||||||
|
|
||||||
/** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
|
/** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
|
||||||
@ -5946,10 +5979,10 @@ export function applyOnGainAbAttrs(pokemon: Pokemon, passive: boolean = false, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one
|
* Clears primal weather/neutralizing gas during the turn if {@linkcode pokemon}'s ability corresponds to one
|
||||||
*/
|
*/
|
||||||
export function applyOnLoseClearWeatherAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void {
|
export function applyOnLoseAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void {
|
||||||
applySingleAbAttrs<PreLeaveFieldClearWeatherAbAttr>(pokemon, passive, PreLeaveFieldClearWeatherAbAttr, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated);
|
applySingleAbAttrs<PreLeaveFieldAbAttr>(pokemon, passive, PreLeaveFieldAbAttr, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated);
|
||||||
}
|
}
|
||||||
function queueShowAbility(pokemon: Pokemon, passive: boolean): void {
|
function queueShowAbility(pokemon: Pokemon, passive: boolean): void {
|
||||||
globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive));
|
globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive));
|
||||||
@ -6838,12 +6871,11 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.GORILLA_TACTICS, 8)
|
new Ability(Abilities.GORILLA_TACTICS, 8)
|
||||||
.attr(GorillaTacticsAbAttr),
|
.attr(GorillaTacticsAbAttr),
|
||||||
new Ability(Abilities.NEUTRALIZING_GAS, 8)
|
new Ability(Abilities.NEUTRALIZING_GAS, 8)
|
||||||
.attr(SuppressFieldAbilitiesAbAttr)
|
.attr(PostSummonAddArenaTagAbAttr, ArenaTagType.NEUTRALIZING_GAS, 0)
|
||||||
|
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)
|
||||||
.attr(UncopiableAbilityAbAttr)
|
.attr(UncopiableAbilityAbAttr)
|
||||||
.attr(UnswappableAbilityAbAttr)
|
.attr(UnswappableAbilityAbAttr)
|
||||||
.attr(NoTransformAbilityAbAttr)
|
.attr(NoTransformAbilityAbAttr),
|
||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonNeutralizingGas", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
|
||||||
.partial(), // A bunch of weird interactions with other abilities being suppressed then unsuppressed
|
|
||||||
new Ability(Abilities.PASTEL_VEIL, 8)
|
new Ability(Abilities.PASTEL_VEIL, 8)
|
||||||
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
|
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
|
||||||
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
|
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
|
||||||
|
@ -8,7 +8,7 @@ import type Pokemon from "#app/field/pokemon";
|
|||||||
import { HitResult, PokemonMove } from "#app/field/pokemon";
|
import { HitResult, PokemonMove } from "#app/field/pokemon";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import type { BattlerIndex } from "#app/battle";
|
import type { BattlerIndex } from "#app/battle";
|
||||||
import { BlockNonDirectDamageAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
|
import { BlockNonDirectDamageAbAttr, InfiltratorAbAttr, PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, ProtectStatAbAttr, applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#app/data/ability";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
|
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -1221,6 +1221,69 @@ export class FairyLockTag extends ArenaTag {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arena tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Neutralizing_Gas_(Ability) Neutralizing Gas}
|
||||||
|
*
|
||||||
|
* Keeps track of the number of pokemon on the field with Neutralizing Gas - If it drops to zero, the effect is ended and abilities are reactivated
|
||||||
|
*
|
||||||
|
* Additionally ends onLose abilities when it is activated
|
||||||
|
*/
|
||||||
|
export class SuppressAbilitiesTag extends ArenaTag {
|
||||||
|
private sourceCount: number;
|
||||||
|
|
||||||
|
constructor(sourceId: number) {
|
||||||
|
super(ArenaTagType.NEUTRALIZING_GAS, 0, undefined, sourceId);
|
||||||
|
this.sourceCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override onAdd(arena: Arena): void {
|
||||||
|
const pokemon = this.getSourcePokemon();
|
||||||
|
if (pokemon) {
|
||||||
|
globalScene.queueMessage(i18next.t("arenaTag:neutralizingGasOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||||
|
|
||||||
|
for (const fieldPokemon of globalScene.getField()) {
|
||||||
|
if (fieldPokemon && fieldPokemon.id !== pokemon.id) {
|
||||||
|
[ true, false ].forEach((passive) => applyOnLoseAbAttrs(fieldPokemon, passive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override onOverlap(arena: Arena): void {
|
||||||
|
this.sourceCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSourceLeave(arena: Arena): void {
|
||||||
|
this.sourceCount--;
|
||||||
|
if (this.sourceCount <= 0) {
|
||||||
|
arena.removeTag(ArenaTagType.NEUTRALIZING_GAS);
|
||||||
|
} else if (this.sourceCount === 1) {
|
||||||
|
// With 1 source left, that pokemon's other abilities should reactivate
|
||||||
|
// This may be confusing for players but would be the most accurate gameplay-wise
|
||||||
|
// Could have a custom message that plays when a specific pokemon's NG ends? This entire thing exists due to passives after all
|
||||||
|
const setter = globalScene.getField().filter((p) => p && p.hasAbilityWithAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, false))[0];
|
||||||
|
applyOnGainAbAttrs(setter, setter.getAbility().hasAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override onRemove(arena: Arena, quiet: boolean = false) {
|
||||||
|
if (!quiet) {
|
||||||
|
globalScene.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pokemon of globalScene.getField()) {
|
||||||
|
// There is only one pokemon with this attr on the field on removal, so its abilities are already active
|
||||||
|
if (pokemon && !pokemon.hasAbilityWithAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, false)) {
|
||||||
|
[ true, false ].forEach((passive) => applyOnGainAbAttrs(pokemon, passive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldApplyToSelf(): boolean {
|
||||||
|
return this.sourceCount > 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
|
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
|
||||||
export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
|
export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
|
||||||
switch (tagType) {
|
switch (tagType) {
|
||||||
@ -1281,6 +1344,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
|
|||||||
return new GrassWaterPledgeTag(sourceId, side);
|
return new GrassWaterPledgeTag(sourceId, side);
|
||||||
case ArenaTagType.FAIRY_LOCK:
|
case ArenaTagType.FAIRY_LOCK:
|
||||||
return new FairyLockTag(turnCount, sourceId);
|
return new FairyLockTag(turnCount, sourceId);
|
||||||
|
case ArenaTagType.NEUTRALIZING_GAS:
|
||||||
|
return new SuppressAbilitiesTag(sourceId);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -7811,11 +7811,12 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
target.summonData.abilitySuppressed = true;
|
|
||||||
globalScene.arena.triggerWeatherBasedFormChangesToNormal();
|
|
||||||
|
|
||||||
globalScene.queueMessage(i18next.t("moveTriggers:suppressAbilities", { pokemonName: getPokemonNameWithAffix(target) }));
|
globalScene.queueMessage(i18next.t("moveTriggers:suppressAbilities", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||||
|
|
||||||
|
target.suppressAbility();
|
||||||
|
|
||||||
|
globalScene.arena.triggerWeatherBasedFormChangesToNormal();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,4 +29,5 @@ export enum ArenaTagType {
|
|||||||
WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE",
|
WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE",
|
||||||
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
|
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
|
||||||
FAIRY_LOCK = "FAIRY_LOCK",
|
FAIRY_LOCK = "FAIRY_LOCK",
|
||||||
|
NEUTRALIZING_GAS = "NEUTRALIZING_GAS"
|
||||||
}
|
}
|
||||||
|
@ -588,8 +588,8 @@ export class Arena {
|
|||||||
// creates a new tag object
|
// creates a new tag object
|
||||||
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, targetIndex, side);
|
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, targetIndex, side);
|
||||||
if (newTag) {
|
if (newTag) {
|
||||||
this.tags.push(newTag);
|
|
||||||
newTag.onAdd(this, quiet);
|
newTag.onAdd(this, quiet);
|
||||||
|
this.tags.push(newTag);
|
||||||
|
|
||||||
const { layers = 0, maxLayers = 0 } = newTag instanceof ArenaTrapTag ? newTag : {};
|
const { layers = 0, maxLayers = 0 } = newTag instanceof ArenaTrapTag ? newTag : {};
|
||||||
|
|
||||||
|
@ -63,8 +63,9 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
|
|||||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
||||||
|
import type { SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||||
import type { Ability, AbAttr } from "#app/data/ability";
|
import type { Ability, AbAttr } from "#app/data/ability";
|
||||||
import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnLoseClearWeatherAbAttrs } from "#app/data/ability";
|
import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnLoseAbAttrs, PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr } from "#app/data/ability";
|
||||||
import type PokemonData from "#app/system/pokemon-data";
|
import type PokemonData from "#app/system/pokemon-data";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { Mode } from "#app/ui/ui";
|
import { Mode } from "#app/ui/ui";
|
||||||
@ -1487,7 +1488,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param ability New Ability
|
* @param ability New Ability
|
||||||
*/
|
*/
|
||||||
public setTempAbility(ability: Ability, passive: boolean = false): void {
|
public setTempAbility(ability: Ability, passive: boolean = false): void {
|
||||||
applyOnLoseClearWeatherAbAttrs(this, passive);
|
applyOnLoseAbAttrs(this, passive);
|
||||||
if (passive) {
|
if (passive) {
|
||||||
this.summonData.passiveAbility = ability.id;
|
this.summonData.passiveAbility = ability.id;
|
||||||
} else {
|
} else {
|
||||||
@ -1496,6 +1497,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
applyOnGainAbAttrs(this, passive);
|
applyOnGainAbAttrs(this, passive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suppresses an ability and calls its onlose attributes
|
||||||
|
*/
|
||||||
|
public suppressAbility() {
|
||||||
|
this.summonData.abilitySuppressed = true;
|
||||||
|
[ true, false ].forEach((passive) => applyOnLoseAbAttrs(this, passive));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a pokemon has a passive either from:
|
* Checks if a pokemon has a passive either from:
|
||||||
* - bought with starter candy
|
* - bought with starter candy
|
||||||
@ -1553,17 +1562,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (this.summonData?.abilitySuppressed && !ability.hasAttr(UnsuppressableAbilityAbAttr)) {
|
if (this.summonData?.abilitySuppressed && !ability.hasAttr(UnsuppressableAbilityAbAttr)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.isOnField() && !ability.hasAttr(SuppressFieldAbilitiesAbAttr)) {
|
const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag;
|
||||||
const suppressed = new Utils.BooleanHolder(false);
|
if (this.isOnField() && suppressAbilitiesTag) {
|
||||||
globalScene.getField(true).filter(p => p !== this).map(p => {
|
const thisAbilitySuppressing = ability.hasAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr);
|
||||||
if (p.getAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility()) {
|
const hasSuppressingAbility = this.hasAbilityWithAttr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr, false);
|
||||||
p.getAbility().getAttrs(SuppressFieldAbilitiesAbAttr).map(a => a.apply(this, false, false, suppressed, [ ability ]));
|
// Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas
|
||||||
}
|
// (Balance decided that the other ability of a neutralizing gas pokemon should not be neutralized)
|
||||||
if (p.getPassiveAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility(true)) {
|
// If the ability itself is neutralizing gas, don't suppress it (handled through arena tag)
|
||||||
p.getPassiveAbility().getAttrs(SuppressFieldAbilitiesAbAttr).map(a => a.apply(this, true, false, suppressed, [ ability ]));
|
const unsuppressable = ability.hasAttr(UnsuppressableAbilityAbAttr) || thisAbilitySuppressing || (hasSuppressingAbility && !suppressAbilitiesTag.shouldApplyToSelf());
|
||||||
}
|
if (!unsuppressable) {
|
||||||
});
|
|
||||||
if (suppressed.value) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
138
test/abilities/neutralizing_gas.test.ts
Normal file
138
test/abilities/neutralizing_gas.test.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Abilities - Neutralizing Gas", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.moveset([ Moves.SPLASH ])
|
||||||
|
.ability(Abilities.NEUTRALIZING_GAS)
|
||||||
|
.battleType("single")
|
||||||
|
.disableCrits()
|
||||||
|
.enemySpecies(Species.MAGIKARP)
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset(Moves.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent other abilities from activating", async () => {
|
||||||
|
game.override.enemyAbility(Abilities.INTIMIDATE);
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
// Intimidate is suppressed, so the attack stat should not be lowered
|
||||||
|
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow the user's passive to activate", async () => {
|
||||||
|
game.override.passiveAbility(Abilities.INTREPID_SWORD);
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.todo("should activate before other abilities", async () => {
|
||||||
|
game.override.enemySpecies(Species.ACCELGOR)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemyAbility(Abilities.INTIMIDATE);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
// Intimidate is suppressed even when the user's speed is lower
|
||||||
|
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should activate other abilities when removed", async () => {
|
||||||
|
game.override.enemyAbility(Abilities.INTREPID_SWORD)
|
||||||
|
.enemyPassiveAbility(Abilities.DAUNTLESS_SHIELD)
|
||||||
|
.enemyMoveset(Moves.ENTRAINMENT);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(0);
|
||||||
|
expect(enemyPokemon?.getStatStage(Stat.DEF)).toBe(0);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
// Enemy removes user's ability, so both abilities are activated
|
||||||
|
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
expect(enemyPokemon?.getStatStage(Stat.DEF)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not activate the user's other ability when removed", async () => {
|
||||||
|
game.override.passiveAbility(Abilities.INTIMIDATE)
|
||||||
|
.enemyMoveset(Moves.ENTRAINMENT);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
// Neutralising gas user's passive is still active
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
// Intimidate did not reactivate after neutralizing gas was removed
|
||||||
|
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only deactivate when all setters are off the field", async () => {
|
||||||
|
game.override.enemyMoveset([ Moves.ENTRAINMENT, Moves.SPLASH ])
|
||||||
|
.battleType("double");
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.ACCELGOR, Species.ACCELGOR ]);
|
||||||
|
game.move.select(Moves.SPLASH, 0);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.ENTRAINMENT, BattlerIndex.PLAYER);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined(); // Now one neut gas user is left
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH, 0);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.ENTRAINMENT, BattlerIndex.PLAYER_2);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); // No neut gas users are left
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deactivate when suppressed by gastro acid", async () => {
|
||||||
|
game.override.enemyMoveset(Moves.GASTRO_ACID);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user