From 4078518c5f36e96116c1b7f68b5c4c689ecca640 Mon Sep 17 00:00:00 2001 From: Xavion3 Date: Sun, 14 Apr 2024 13:21:34 +1000 Subject: [PATCH] Add support for ability changing effects (#113) * Add support for ability changing effects * Fix doodle with a fainted ally in doubles --- src/data/ability.ts | 107 +++++++++++++++++++++++++++++++++++++++----- src/data/move.ts | 105 ++++++++++++++++++++++++++++++++++++++++--- src/phases.ts | 2 +- 3 files changed, 196 insertions(+), 18 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 072a6aa15..eb83d53e2 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -572,6 +572,47 @@ export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr { } } +export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr { + constructor() { + super(); + } + + applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { + if (move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr)) { + const tempAbilityId = attacker.getAbility().id; + attacker.summonData.ability = pokemon.getAbility().id; + pokemon.summonData.ability = tempAbilityId; + return true; + } + + return false; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return getPokemonMessage(pokemon, ` swapped\nabilities with its target!`); + } +} + +export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { + constructor() { + super(); + } + + applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { + if (move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr)) { + attacker.summonData.ability = pokemon.getAbility().id; + + return true; + } + + return false; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return getPokemonMessage(pokemon, ` gave its target\n${abilityName}!`); + } +} + export class PreAttackAbAttr extends AbAttr { applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: PokemonMove, args: any[]): boolean | Promise { return false; @@ -829,7 +870,7 @@ class PostVictoryStatChangeAbAttr extends PostVictoryAbAttr { } export class PostKnockOutAbAttr extends AbAttr { - applyPostKnockOut(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { + applyPostKnockOut(pokemon: Pokemon, passive: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { return false; } } @@ -845,7 +886,7 @@ export class PostKnockOutStatChangeAbAttr extends PostKnockOutAbAttr { this.levels = levels; } - applyPostKnockOut(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { + applyPostKnockOut(pokemon: Pokemon, passive: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { const stat = typeof this.stat === 'function' ? this.stat(pokemon) : this.stat; @@ -855,6 +896,22 @@ export class PostKnockOutStatChangeAbAttr extends PostKnockOutAbAttr { } } +export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { + constructor() { + super(); + } + + applyPostKnockOut(pokemon: Pokemon, passive: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { + if (pokemon.isPlayer() === knockedOut.isPlayer() && !knockedOut.getAbility().hasAttr(UncopiableAbilityAbAttr)) { + pokemon.summonData.ability = knockedOut.getAbility().id; + pokemon.scene.queueMessage(getPokemonMessage(knockedOut, `'s ${allAbilities[knockedOut.getAbility().id].name} was taken over!`)); + return true; + } + + return false; + } +} + export class IgnoreOpponentStatChangesAbAttr extends AbAttr { constructor() { super(false); @@ -1018,6 +1075,27 @@ export class PostSummonFormChangeAbAttr extends PostSummonAbAttr { } } +export class TraceAbAttr extends PostSummonAbAttr { + applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { + const targets = pokemon.getOpponents(); + let target: Pokemon; + if (targets.length > 1) + pokemon.scene.executeWithSeedOffset(() => target = Utils.randSeedItem(targets), pokemon.scene.currentBattle.waveIndex); + else + target = targets[0]; + + // Wonder Guard is normally uncopiable so has the attribute, but trace specifically can copy it + if (target.getAbility().hasAttr(UncopiableAbilityAbAttr) && target.getAbility().id !== Abilities.WONDER_GUARD) + return false; + + pokemon.summonData.ability = target.getAbility().id; + + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` traced ${target.name}'s\n${allAbilities[target.getAbility().id].name}!`)); + + return true; + } +} + export class PostSummonTransformAbAttr extends PostSummonAbAttr { constructor() { super(true); @@ -1900,8 +1978,8 @@ export function applyPostAttackAbAttrs(attrType: { new(...args: any[]): PostAtta } export function applyPostKnockOutAbAttrs(attrType: { new(...args: any[]): PostKnockOutAbAttr }, - pokemon: Pokemon, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostKnockOut(pokemon, passive, args), args); + pokemon: Pokemon, knockedOut: Pokemon, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostKnockOut(pokemon, passive, knockedOut, args), args); } export function applyPostVictoryAbAttrs(attrType: { new(...args: any[]): PostVictoryAbAttr }, @@ -2085,7 +2163,8 @@ export function initAbilities() { .attr(ProtectStatAbAttr, BattleStat.ACC) .attr(DoubleBattleChanceAbAttr) .ignorable(), - new Ability(Abilities.TRACE, "Trace (N)", "When it enters a battle, the Pokémon copies an opposing Pokémon's Ability.", 3) + new Ability(Abilities.TRACE, "Trace", "When it enters a battle, the Pokémon copies an opposing Pokémon's Ability.", 3) + .attr(TraceAbAttr) .attr(UncopiableAbilityAbAttr), new Ability(Abilities.HUGE_POWER, "Huge Power", "Doubles the Pokémon's Attack stat.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.ATK, 2), @@ -2356,7 +2435,9 @@ export function initAbilities() { .attr(PostSummonTransformAbAttr) .attr(UncopiableAbilityAbAttr), new Ability(Abilities.INFILTRATOR, "Infiltrator (N)", "Passes through the opposing Pokémon's barrier, substitute, and the like and strikes.", 5), - new Ability(Abilities.MUMMY, "Mummy (N)", "Contact with the Pokémon changes the attacker's Ability to Mummy.", 5), + new Ability(Abilities.MUMMY, "Mummy", "Contact with the Pokémon changes the attacker's Ability to Mummy.", 5) + .attr(PostDefendAbilityGiveAbAttr) + .bypassFaint(), new Ability(Abilities.MOXIE, "Moxie", "The Pokémon shows moxie, and that boosts the Attack stat after knocking out any Pokémon.", 5) .attr(PostVictoryStatChangeAbAttr, BattleStat.ATK, 1), new Ability(Abilities.JUSTIFIED, "Justified", "Being hit by a Dark-type move boosts the Attack stat of the Pokémon, for justice.", 5) @@ -2524,9 +2605,11 @@ export function initAbilities() { .attr(PostKnockOutStatChangeAbAttr, BattleStat.SPATK, 1), new Ability(Abilities.TANGLING_HAIR, "Tangling Hair", "Contact with the Pokémon lowers the attacker's Speed stat.", 7) .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), BattleStat.SPD, -1, false), - new Ability(Abilities.RECEIVER, "Receiver (N)", "The Pokémon copies the Ability of a defeated ally.", 7) + new Ability(Abilities.RECEIVER, "Receiver", "The Pokémon copies the Ability of a defeated ally.", 7) + .attr(CopyFaintedAllyAbilityAbAttr) .attr(UncopiableAbilityAbAttr), - new Ability(Abilities.POWER_OF_ALCHEMY, "Power of Alchemy (N)", "The Pokémon copies the Ability of a defeated ally.", 7) + new Ability(Abilities.POWER_OF_ALCHEMY, "Power of Alchemy", "The Pokémon copies the Ability of a defeated ally.", 7) + .attr(CopyFaintedAllyAbilityAbAttr) .attr(UncopiableAbilityAbAttr), new Ability(Abilities.BEAST_BOOST, "Beast Boost", "The Pokémon boosts its most proficient stat each time it knocks out a Pokémon.", 7) .attr(PostVictoryStatChangeAbAttr, p => { @@ -2604,7 +2687,9 @@ export function initAbilities() { new Ability(Abilities.SCREEN_CLEANER, "Screen Cleaner (N)", "When the Pokémon enters a battle, the effects of Light Screen, Reflect, and Aurora Veil are nullified for both opposing and ally Pokémon.", 8), new Ability(Abilities.STEELY_SPIRIT, "Steely Spirit (N)", "Powers up ally Pokémon's Steel-type moves.", 8), new Ability(Abilities.PERISH_BODY, "Perish Body (N)", "When hit by a move that makes direct contact, the Pokémon and the attacker will faint after three turns unless they switch out of battle.", 8), - new Ability(Abilities.WANDERING_SPIRIT, "Wandering Spirit (N)", "The Pokémon exchanges Abilities with a Pokémon that hits it with a move that makes direct contact.", 8), + new Ability(Abilities.WANDERING_SPIRIT, "Wandering Spirit (P)", "The Pokémon exchanges Abilities with a Pokémon that hits it with a move that makes direct contact.", 8) + .attr(PostDefendAbilitySwapAbAttr) + .bypassFaint(), new Ability(Abilities.GORILLA_TACTICS, "Gorilla Tactics (N)", "Boosts the Pokémon's Attack stat but only allows the use of the first selected move.", 8), new Ability(Abilities.NEUTRALIZING_GAS, "Neutralizing Gas (N)", "If the Pokémon with Neutralizing Gas is in the battle, the effects of all Pokémon's Abilities will be nullified or will not be triggered.", 8) .attr(UncopiableAbilityAbAttr) @@ -2642,7 +2727,9 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr), - new Ability(Abilities.LINGERING_AROMA, "Lingering Aroma (N)", "Contact with the Pokémon changes the attacker's Ability to Lingering Aroma.", 9), + new Ability(Abilities.LINGERING_AROMA, "Lingering Aroma", "Contact with the Pokémon changes the attacker's Ability to Lingering Aroma.", 9) + .attr(PostDefendAbilityGiveAbAttr) + .bypassFaint(), new Ability(Abilities.SEED_SOWER, "Seed Sower", "Turns the ground into Grassy Terrain when the Pokémon is hit by an attack.", 9) .attr(PostDefendTerrainChangeAbAttr, TerrainType.GRASSY), new Ability(Abilities.THERMAL_EXCHANGE, "Thermal Exchange (P)", "Boosts the Attack stat when the Pokémon is hit by a Fire-type move. The Pokémon also cannot be burned.", 9) diff --git a/src/data/move.ts b/src/data/move.ts index 35687050b..8ea1df3cf 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -12,8 +12,9 @@ import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagType } from "./enums/arena-tag-type"; -import { UnswappableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr } from "./ability"; +import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, NoTransformAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr } from "./ability"; import { Abilities } from "./enums/abilities"; +import { allAbilities } from './ability'; import { PokemonHeldItemModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; import { Stat } from "./pokemon-stat"; @@ -2914,6 +2915,91 @@ export class SketchAttr extends MoveEffectAttr { } } +export class AbilityChangeAttr extends MoveEffectAttr { + public ability: Abilities; + + constructor(ability: Abilities, selfTarget?: boolean) { + super(selfTarget, MoveEffectTrigger.HIT); + + this.ability = ability; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) + return false; + + (this.selfTarget ? user : target).summonData.ability = this.ability; + + user.scene.queueMessage('The ' + getPokemonMessage((this.selfTarget ? user : target), ` acquired\n${allAbilities[this.ability].name}!`)); + + return true; + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => !(this.selfTarget ? user : target).getAbility().hasAttr(UnsuppressableAbilityAbAttr) && (this.selfTarget ? user : target).getAbility().id !== this.ability; + } +} + +export class AbilityCopyAttr extends MoveEffectAttr { + public copyToPartner: boolean; + + constructor(copyToPartner: boolean = false) { + super(false, MoveEffectTrigger.HIT); + + this.copyToPartner = copyToPartner; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) + return false; + + user.summonData.ability = target.getAbility().id; + + user.scene.queueMessage(getPokemonMessage(user, ` copied the `) + getPokemonMessage(target, `'s\n${allAbilities[target.getAbility().id].name}!`)); + + if (this.copyToPartner && user.scene.currentBattle?.double && user.getAlly().hp) { + user.getAlly().summonData.ability = target.getAbility().id; + user.getAlly().scene.queueMessage(getPokemonMessage(user.getAlly(), ` copied the `) + getPokemonMessage(target, `'s\n${allAbilities[target.getAbility().id].name}!`)); + } + + return true; + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => { + let ret = !target.getAbility().hasAttr(UncopiableAbilityAbAttr) && !user.getAbility().hasAttr(UnsuppressableAbilityAbAttr); + if (this.copyToPartner && user.scene.currentBattle?.double) + ret = ret && (!user.getAlly().hp || !user.getAlly().getAbility().hasAttr(UnsuppressableAbilityAbAttr)); + else + ret = ret && user.getAbility().id !== target.getAbility().id; + return ret; + }; + } +} + +export class AbilityGiveAttr extends MoveEffectAttr { + public copyToPartner: boolean; + + constructor() { + super(false, MoveEffectTrigger.HIT); + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) + return false; + + target.summonData.ability = user.getAbility().id; + + user.scene.queueMessage('The' + getPokemonMessage(target, `\nacquired ${allAbilities[user.getAbility().id].name}!`)); + + return true; + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => !user.getAbility().hasAttr(UncopiableAbilityAbAttr) && !target.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && user.getAbility().id !== target.getAbility().id; + } +} + export class SwitchAbilitiesAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) @@ -2923,7 +3009,7 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { user.summonData.ability = target.getAbility().id; target.summonData.ability = tempAbilityId; - user.scene.queueMessage(getPokemonMessage(user, ` swapped\nAbilities with its target!`)); + user.scene.queueMessage(getPokemonMessage(user, ` swapped\nabilities with its target!`)); return true; } @@ -3794,7 +3880,8 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.TRICK, "Trick (N)", Type.PSYCHIC, 100, 10, "The user catches the target off guard and swaps its held item with its own.", -1, 0, 3), - new StatusMove(Moves.ROLE_PLAY, "Role Play (N)", Type.PSYCHIC, -1, 10, "The user mimics the target completely, copying the target's Ability.", -1, 0, 3), + new StatusMove(Moves.ROLE_PLAY, "Role Play", Type.PSYCHIC, -1, 10, "The user mimics the target completely, copying the target's Ability.", -1, 0, 3) + .attr(AbilityCopyAttr), new SelfStatusMove(Moves.WISH, "Wish (N)", Type.NORMAL, -1, 10, "One turn after this move is used, the user's or its replacement's HP is restored by half the user's max HP.", -1, 0, 3) .triageMove(), new SelfStatusMove(Moves.ASSIST, "Assist", Type.NORMAL, -1, 20, "The user hurriedly and randomly uses a move among those known by ally Pokémon.", -1, 0, 3) @@ -4076,7 +4163,8 @@ export function initMoves() { }); return uniqueUsedMoveIds.size >= movesetMoveIds.length - 1; }), - new StatusMove(Moves.WORRY_SEED, "Worry Seed (N)", Type.GRASS, 100, 10, "A seed that causes worry is planted on the target. It prevents sleep by making the target's Ability Insomnia.", -1, 0, 4), + new StatusMove(Moves.WORRY_SEED, "Worry Seed", Type.GRASS, 100, 10, "A seed that causes worry is planted on the target. It prevents sleep by making the target's Ability Insomnia.", -1, 0, 4) + .attr(AbilityChangeAttr, Abilities.INSOMNIA), new AttackMove(Moves.SUCKER_PUNCH, "Sucker Punch (P)", Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, "This move enables the user to attack first. This move fails if the target is not readying an attack.", -1, 1, 4), new StatusMove(Moves.TOXIC_SPIKES, "Toxic Spikes", Type.POISON, -1, 20, "The user lays a trap of poison spikes at the feet of the opposing team. The spikes will poison opposing Pokémon that switch into battle.", -1, 0, 4) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) @@ -4325,8 +4413,10 @@ export function initMoves() { .ballBombMove(), new AttackMove(Moves.FOUL_PLAY, "Foul Play", Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, "The user turns the target's power against it. The higher the target's Attack stat, the greater the damage it deals.", -1, 0, 5) .attr(TargetAtkUserAtkAttr), - new StatusMove(Moves.SIMPLE_BEAM, "Simple Beam (N)", Type.NORMAL, 100, 15, "The user's mysterious psychic wave changes the target's Ability to Simple.", -1, 0, 5), - new StatusMove(Moves.ENTRAINMENT, "Entrainment (N)", Type.NORMAL, 100, 15, "The user dances with an odd rhythm that compels the target to mimic it, making the target's Ability the same as the user's.", -1, 0, 5), + new StatusMove(Moves.SIMPLE_BEAM, "Simple Beam", Type.NORMAL, 100, 15, "The user's mysterious psychic wave changes the target's Ability to Simple.", -1, 0, 5) + .attr(AbilityChangeAttr, Abilities.SIMPLE), + new StatusMove(Moves.ENTRAINMENT, "Entrainment", Type.NORMAL, 100, 15, "The user dances with an odd rhythm that compels the target to mimic it, making the target's Ability the same as the user's.", -1, 0, 5) + .attr(AbilityGiveAttr), new StatusMove(Moves.AFTER_YOU, "After You (N)", Type.NORMAL, -1, 15, "The user helps the target and makes it use its move right after the user.", -1, 0, 5) .ignoresProtect(), new AttackMove(Moves.ROUND, "Round (P)", Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, "The user attacks the target with a song. Others can join in the Round to increase the power of the attack.", -1, 0, 5) @@ -5237,7 +5327,8 @@ export function initMoves() { .attr(LapseBattlerTagAttr, [ BattlerTagType.BIND, BattlerTagType.WRAP, BattlerTagType.FIRE_SPIN, BattlerTagType.WHIRLPOOL, BattlerTagType.CLAMP, BattlerTagType.SAND_TOMB, BattlerTagType.MAGMA_STORM, BattlerTagType.THUNDER_CAGE, BattlerTagType.SEEDED ], true) .attr(StatusEffectAttr, StatusEffect.POISON) .target(MoveTarget.ALL_NEAR_ENEMIES), - new StatusMove(Moves.DOODLE, "Doodle (N)", Type.NORMAL, 100, 10, "The user captures the very essence of the target in a sketch. This changes the Abilities of the user and its ally Pokémon to that of the target.", -1, 0, 9), + new StatusMove(Moves.DOODLE, "Doodle", Type.NORMAL, 100, 10, "The user captures the very essence of the target in a sketch. This changes the Abilities of the user and its ally Pokémon to that of the target.", -1, 0, 9) + .attr(AbilityCopyAttr, true), new SelfStatusMove(Moves.FILLET_AWAY, "Fillet Away", Type.NORMAL, -1, 10, "The user sharply boosts its Attack, Sp. Atk, and Speed stats by using its own HP.", -1, 0, 9) .attr(CutHpStatBoostAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.SPD ], 2, 2), new AttackMove(Moves.KOWTOW_CLEAVE, "Kowtow Cleave", Type.DARK, MoveCategory.PHYSICAL, 85, -1, 10, "The user slashes at the target after kowtowing to make the target let down its guard. This attack never misses.", -1, 0, 9) diff --git a/src/phases.ts b/src/phases.ts index 092fa7e5d..4e065265c 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2975,7 +2975,7 @@ export class FaintPhase extends PokemonPhase { } const alivePlayField = this.scene.getField(true); - alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p)); + alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon)); if (pokemon.turnData?.attacksReceived?.length) { const defeatSource = this.scene.getPokemonById(pokemon.turnData.attacksReceived[0].sourceId); if (defeatSource?.isOnField())