From 80ee440109e707e37dbec75d8e74657903ec9ae7 Mon Sep 17 00:00:00 2001 From: Madi Simpson Date: Sun, 5 May 2024 07:52:27 -0700 Subject: [PATCH] Implement Guard Dog, attribute for abilities to give Intimidate immunity (#448) * abilities: implement guard dog, abilities that give intimidate immunity * abilities: implement rattled's second effect, remove refs to mold breaker * abilities: fix rattled not giving the attack drop still * abilities: make ability bars pop in to some success * abilities: implement suction cups since it has the same effect * moves: add custom fail text, fix animation issues with Guard Dog/Roar * abilities: manually show intimidate ability bar to prevent weirdness --- src/data/ability.ts | 96 ++++++++++++++++++++++++++++++++++----------- src/data/move.ts | 33 ++++++++++++---- src/phases.ts | 9 +++-- 3 files changed, 104 insertions(+), 34 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 57d1402ad8d..28e089ddde2 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -20,6 +20,8 @@ import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; import { Abilities } from "./enums/abilities"; import i18next, { Localizable } from "#app/plugins/i18n.js"; import { Command } from "../ui/command-ui-handler"; +import Battle from "#app/battle.js"; +import { ability } from "#app/locales/en/ability.js"; export class Ability implements Localizable { public id: Abilities; @@ -1271,6 +1273,40 @@ export class IgnoreOpponentStatChangesAbAttr extends AbAttr { } } +export class IntimidateImmunityAbAttr extends AbAttr { + constructor() { + super(false); + } + + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + cancelled.value = true; + return true; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return getPokemonMessage(pokemon, `'s ${abilityName} prevented it from being Intimidated!`); + } +} + +export class PostIntimidateStatChangeAbAttr extends AbAttr { + private stats: BattleStat[]; + private levels: integer; + private overwrites: boolean; + + constructor(stats: BattleStat[], levels: integer, overwrites?: boolean) { + super(true) + this.stats = stats + this.levels = levels + this.overwrites = !!overwrites + } + + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + pokemon.scene.pushPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, this.stats, this.levels)); + cancelled.value = this.overwrites; + return true; + } +} + export class PostSummonAbAttr extends AbAttr { applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { return false; @@ -1313,34 +1349,36 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { private stats: BattleStat[]; private levels: integer; private selfTarget: boolean; + private intimidate: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean) { - super(); + constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, intimidate?: boolean) { + super(false); this.stats = typeof(stats) === 'number' ? [ stats as BattleStat ] : stats as BattleStat[]; this.levels = levels; this.selfTarget = !!selfTarget; + this.intimidate = !!intimidate; } applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { - const statChangePhases: StatChangePhase[] = []; - - if (this.selfTarget) - statChangePhases.push(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); - else { - for (let opponent of pokemon.getOpponents()) - statChangePhases.push(new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels)); + queueShowAbility(pokemon, passive); // TODO: Better solution than manually showing the ability here + if (this.selfTarget) { + pokemon.scene.pushPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + return true; } - - for (let statChangePhase of statChangePhases) { - if (!this.selfTarget && !statChangePhase.getPokemon().summonData) - pokemon.scene.pushPhase(statChangePhase); // TODO: This causes the ability bar to be shown at the wrong time - else + for (let opponent of pokemon.getOpponents()) { + const cancelled = new Utils.BooleanHolder(false) + if (this.intimidate) { + applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled); + applyAbAttrs(PostIntimidateStatChangeAbAttr, opponent, cancelled); + } + if (!cancelled.value) { + const statChangePhase = new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels); pokemon.scene.unshiftPhase(statChangePhase); + } } - return true; } } @@ -2316,6 +2354,13 @@ export class FlinchStatChangeAbAttr extends FlinchEffectAbAttr { export class IncreasePpAbAttr extends AbAttr { } +export class ForceSwitchOutImmunityAbAttr extends AbAttr { + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` can't be switched out!`)) + return true; + } +} + export class ReduceBerryUseThresholdAbAttr extends AbAttr { constructor() { super(); @@ -2656,6 +2701,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.OBLIVIOUS, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED) + .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.CLOUD_NINE, 3) .attr(SuppressWeatherEffectAbAttr, true), @@ -2678,12 +2724,13 @@ export function initAbilities() { .unimplemented(), new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) + .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.SUCTION_CUPS, 3) - .ignorable() - .unimplemented(), + .attr(ForceSwitchOutImmunityAbAttr) + .ignorable(), new Ability(Abilities.INTIMIDATE, 3) - .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, -1), + .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, -1, false, true), new Ability(Abilities.SHADOW_TAG, 3) .attr(ArenaTrapAbAttr), new Ability(Abilities.ROUGH_SKIN, 3) @@ -2732,6 +2779,7 @@ export function initAbilities() { .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON), new Ability(Abilities.INNER_FOCUS, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.FLINCHED) + .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.MAGMA_ARMOR, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE) @@ -2928,8 +2976,9 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.SLOW_START, 4) .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.SLOW_START, 5), - new Ability(Abilities.SCRAPPY, 4) - .unimplemented(), + new Ability(Abilities.SCRAPPY, 4) + .attr(IntimidateImmunityAbAttr) + .partial(), new Ability(Abilities.STORM_DRAIN, 4) .attr(RedirectTypeMoveAbAttr, Type.WATER) .attr(TypeImmunityStatChangeAbAttr, Type.WATER, BattleStat.SPATK, 1) @@ -3048,7 +3097,7 @@ export function initAbilities() { new Ability(Abilities.RATTLED, 5) .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS && (move.type === Type.DARK || move.type === Type.BUG || move.type === Type.GHOST), BattleStat.SPD, 1) - .partial(), + .attr(PostIntimidateStatChangeAbAttr, [BattleStat.SPD], 1), new Ability(Abilities.MAGIC_BOUNCE, 5) .ignorable() .unimplemented(), @@ -3433,8 +3482,9 @@ export function initAbilities() { .ignorable() .partial(), new Ability(Abilities.GUARD_DOG, 9) - .ignorable() - .unimplemented(), + .attr(PostIntimidateStatChangeAbAttr, [BattleStat.ATK], 1, true) + .attr(ForceSwitchOutImmunityAbAttr) + .ignorable(), new Ability(Abilities.ROCKY_PAYLOAD, 9) .attr(MoveTypePowerBoostAbAttr, Type.ROCK), new Ability(Abilities.WIND_POWER, 9) diff --git a/src/data/move.ts b/src/data/move.ts index f5fc8e3ebcb..17d1a461fce 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -12,7 +12,7 @@ import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagType } from "./enums/arena-tag-type"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, NoTransformAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr } from "./ability"; +import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, NoTransformAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr } from "./ability"; import { Abilities } from "./enums/abilities"; import { allAbilities } from './ability'; import { PokemonHeldItemModifier } from "../modifier/modifier"; @@ -326,6 +326,13 @@ export default class Move implements Localizable { return true; } + getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + let failedText = null; + for (let attr of this.attrs) + failedText = attr.getFailedText(user, target, move, cancelled); + return failedText; + } + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { let score = 0; @@ -422,6 +429,10 @@ export abstract class MoveAttr { return null; } + getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + return null; + } + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { return 0; } @@ -2964,16 +2975,14 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { - if (!this.user && target.isMax()) - return resolve(false); - + // Check if the move category is not STATUS or if the switch out condition is not met - if (move.category !== MoveCategory.STATUS && !this.getSwitchOutCondition()(user, target, move)) { + if (!this.getCondition()(user, target, move)) { //Apply effects before switch out i.e. poison point, flame body, etc applyPostDefendAbAttrs(PostDefendContactApplyStatusEffectAbAttr, target, user, new PokemonMove(move.id), null); return resolve(false); } - + // Move the switch out logic inside the conditional block // This ensures that the switch out only happens when the conditions are met const switchOutTarget = this.user ? user : target; @@ -3020,15 +3029,23 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { resolve(true); }); } - + getCondition(): MoveConditionFunc { - return (user, target, move) => move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move); + return (user, target, move) => (move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move)); + } + + getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, cancelled); + return null; } getSwitchOutCondition(): MoveConditionFunc { return (user, target, move) => { const switchOutTarget = (this.user ? user : target); const player = switchOutTarget instanceof PlayerPokemon; + + if (!this.user && move.category == MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) + return false; if (!player && !user.scene.currentBattle.battleType) { if (this.batonPass) diff --git a/src/phases.ts b/src/phases.ts index 7ae30f0a803..0f583d9a14e 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2278,18 +2278,21 @@ export class MovePhase extends BattlePhase { // Assume conditions affecting targets only apply to moves with a single target let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove()); - let failedText = null; + let cancelled = new Utils.BooleanHolder(true); + let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled); if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) success = false; else if (success && this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, this.move.getMove())) { success = false; - failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain.terrainType); + if (failedText == null) + failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain.terrainType); } if (success) this.scene.unshiftPhase(this.getEffectPhase()); else { this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); - this.showFailedText(failedText); + if (!cancelled.value) + this.showFailedText(failedText); } this.end();