diff --git a/src/battle-phases.ts b/src/battle-phases.ts index ef3b4e8c649..f7def75bde2 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -25,7 +25,7 @@ import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTrapTag, TrickRoomTag } from "./data/arena-tag"; -import { ArenaTrapAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreWeatherDamageAbAttr, ProtectStatAttr, SuppressWeatherEffectAbAttr, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreWeatherEffectAbAttrs } from "./data/ability"; +import { CheckTrappedAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreWeatherDamageAbAttr, ProtectStatAttr, SuppressWeatherEffectAbAttr, applyCheckTrappedAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreWeatherEffectAbAttrs } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; export class CheckLoadPhase extends BattlePhase { @@ -678,13 +678,15 @@ export class CommandPhase extends FieldPhase { break; case Command.POKEMON: const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; - const arenaTrapped = !!enemyPokemon.getAbility().hasAttr(ArenaTrapAbAttr); + const trapped = new Utils.BooleanHolder(false); const batonPass = args[0] as boolean; - if (batonPass || (!trapTag && !arenaTrapped)) { + if (!batonPass) + applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped); + if (batonPass || (!trapTag && !trapped.value)) { this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, cursor, true, args[0] as boolean)); success = true; - } else - this.scene.ui.showText(`${this.scene.getPokemonById(trapTag.sourceId).name}'s ${trapTag?.getMoveName() || enemyPokemon.getAbility().name}\nprevents switching!`, null, () => { + } else if (trapTag) + this.scene.ui.showText(`${this.scene.getPokemonById(trapTag.sourceId).name}'s ${trapTag.getMoveName()}\nprevents switching!`, null, () => { this.scene.ui.showText(null, 0); }, null, true); break; @@ -877,7 +879,7 @@ export abstract class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); - const target = this.pokemon.isPlayer() ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon(); + const target = this.pokemon.getOpponent(); if (!this.followUp && this.canMove()) this.pokemon.lapseTags(BattlerTagLapseType.MOVE); @@ -1043,8 +1045,11 @@ abstract class MoveEffectPhase extends PokemonPhase { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); } // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present - if (!isProtected && !this.move.getMove().getAttrs(ChargeAttr).filter(ca => (ca as ChargeAttr).chargeEffect).length) + if (!isProtected && !this.move.getMove().getAttrs(ChargeAttr).filter(ca => (ca as ChargeAttr).chargeEffect).length) { applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveHitEffectAttr && (!!target.hp || (attr as MoveHitEffectAttr).selfTarget), user, target, this.move.getMove()); + if (target.hp) + applyPostDefendAbAttrs(PostDefendAbAttr, user, target, this.move, result); + } } this.end(); }); diff --git a/src/data/ability.ts b/src/data/ability.ts index 2741d7a7699..629217e9b75 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,13 +1,13 @@ import Pokemon, { MoveResult, PokemonMove } from "../pokemon"; -import { Type, getTypeDamageMultiplier } from "./type"; +import { Type } from "./type"; import * as Utils from "../utils"; import { BattleStat, getBattleStatName } from "./battle-stat"; import { DamagePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../battle-phases"; import { getPokemonMessage } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTagType } from "./battler-tag"; -import { StatusEffect } from "./status-effect"; -import { Moves, RecoilAttr, WeatherHealAttr } from "./move"; +import { BattlerTag, BattlerTagType, TrappedTag } from "./battler-tag"; +import { StatusEffect, getStatusEffectDescriptor } from "./status-effect"; +import { MoveFlags, Moves, RecoilAttr } from "./move"; export class Ability { public id: Abilities; @@ -212,6 +212,54 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { } } +export class PostDefendAbAttr extends AbAttr { + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { + return false; + } +} + +export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { + private chance: integer; + private effects: StatusEffect[]; + + constructor(chance: integer, ...effects: StatusEffect[]) { + super(); + + this.chance = chance; + this.effects = effects; + } + + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { + if (move.getMove().hasFlag(MoveFlags.MAKES_CONTACT) && Utils.randInt(100) < this.chance) { + const effect = this.effects.length === 1 ? this.effects[0] : this.effects[Utils.randInt(this.effects.length)]; + return attacker.trySetStatus(effect); + } + + return false; + } +} + +export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr { + private chance: integer; + private tagType: BattlerTagType; + private turnCount: integer; + + constructor(chance: integer, tagType: BattlerTagType, turnCount?: integer) { + super(); + + this.tagType = tagType; + this.chance = chance; + this.turnCount = turnCount; + } + + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { + if (move.getMove().hasFlag(MoveFlags.MAKES_CONTACT) && Utils.randInt(100) < this.chance) + return attacker.addTag(this.tagType, this.turnCount, move.moveId, pokemon.id); + + return false; + } +} + export class PreAttackAbAttr extends AbAttr { applyPreAttack(pokemon: Pokemon, defender: Pokemon, move: PokemonMove, args: any[]): boolean { return false; @@ -282,6 +330,29 @@ export class BattleStatMultiplierAbAttr extends AbAttr { } } +export class PostSummonAbAttr extends AbAttr { + applyPostSummon(pokemon: Pokemon, args: any[]) { + return false; + } +} + +export class PostSummonWeatherChangeAbAttr extends PostSummonAbAttr { + private weatherType: WeatherType; + + constructor(weatherType: WeatherType) { + super(); + + this.weatherType = weatherType; + } + + applyPostSummon(pokemon: Pokemon, args: any[]): boolean { + if (!pokemon.scene.arena.weather?.isImmutable()) + return pokemon.scene.arena.trySetWeather(this.weatherType, false); + + return false; + } +} + export class PreStatChangeAbAttr extends AbAttr { applyPreStatChange(pokemon: Pokemon, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { return false; @@ -311,9 +382,65 @@ export class ProtectStatAttr extends PreStatChangeAbAttr { } } -export class BlockCritAbAttr extends AbAttr { } +export class PreSetStatusAbAttr extends AbAttr { + applyPreSetStatus(pokemon: Pokemon, effect: StatusEffect, cancelled: Utils.BooleanHolder, args: any[]): boolean { + return false; + } +} -export class ArenaTrapAbAttr extends AbAttr { } +export class StatusEffectImmunityAbAttr extends PreSetStatusAbAttr { + private immuneEffects: StatusEffect[]; + + constructor(...immuneEffects: StatusEffect[]) { + super(); + + this.immuneEffects = immuneEffects; + } + + applyPreSetStatus(pokemon: Pokemon, effect: StatusEffect, cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (!this.immuneEffects.length || this.immuneEffects.indexOf(effect) > -1) { + cancelled.value = true; + return true; + } + + return false; + } + + getTriggerMessage(pokemon: Pokemon, ...args: any[]): string { + return getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nprevents ${this.immuneEffects.length ? getStatusEffectDescriptor(args[0] as StatusEffect) : 'status problems'}!`); + } +} + +export class PreApplyBattlerTagAbAttr extends AbAttr { + applyPreApplyBattlerTag(pokemon: Pokemon, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean { + return false; + } +} + +export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { + private immuneTagType: BattlerTagType; + + constructor(immuneTagType: BattlerTagType) { + super(); + + this.immuneTagType = immuneTagType; + } + + applyPreApplyBattlerTag(pokemon: Pokemon, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (tag.tagType === this.immuneTagType) { + cancelled.value = true; + return true; + } + + return false; + } + + getTriggerMessage(pokemon: Pokemon, ...args: any[]): string { + return getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nprevents ${(args[0] as BattlerTag).getDescriptor()}!`); + } +} + +export class BlockCritAbAttr extends AbAttr { } export class PreWeatherEffectAbAttr extends AbAttr { applyPreWeatherEffect(pokemon: Pokemon, weather: Weather, cancelled: Utils.BooleanHolder, args: any[]): boolean { @@ -453,6 +580,23 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { } } +export class CheckTrappedAbAttr extends AbAttr { + applyCheckTrapped(pokemon: Pokemon, trapped: Utils.BooleanHolder, args: any[]): boolean { + return false; + } +} + +export class ArenaTrapAbAttr extends CheckTrappedAbAttr { + applyCheckTrapped(pokemon: Pokemon, trapped: Utils.BooleanHolder, args: any[]): boolean { + trapped.value = true; + return true; + } + + getTriggerMessage(pokemon: Pokemon, ...args: any[]): string { + return getPokemonMessage(pokemon, `\'s ${pokemon.getAbility().name}\nprevents switching!`); + } +} + export function applyAbAttrs(attrType: { new(...args: any[]): AbAttr }, pokemon: Pokemon, cancelled: Utils.BooleanHolder, ...args: any[]): void { if (!pokemon.canApplyAbility()) return; @@ -497,6 +641,28 @@ export function applyPreDefendAbAttrs(attrType: { new(...args: any[]): PreDefend pokemon.scene.clearPhaseQueueSplice(); } +export function applyPostDefendAbAttrs(attrType: { new(...args: any[]): PostDefendAbAttr }, + pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, ...args: any[]): void { + if (!pokemon.canApplyAbility()) + return; + + const ability = pokemon.getAbility(); + const attrs = ability.getAttrs(attrType) as PostDefendAbAttr[]; + for (let attr of attrs) { + if (!canApplyAttr(pokemon, attr)) + continue; + pokemon.scene.setPhaseQueueSplice(); + if (attr.applyPostDefend(pokemon, attacker, move, moveResult, args)) { + queueShowAbility(pokemon); + const message = attr.getTriggerMessage(pokemon, attacker, move); + if (message) + pokemon.scene.queueMessage(message); + } + } + + pokemon.scene.clearPhaseQueueSplice(); +} + export function applyBattleStatMultiplierAbAttrs(attrType: { new(...args: any[]): BattleStatMultiplierAbAttr }, pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, ...args: any[]) { if (!pokemon.canApplyAbility()) @@ -542,6 +708,28 @@ export function applyPreAttackAbAttrs(attrType: { new(...args: any[]): PreAttack pokemon.scene.clearPhaseQueueSplice(); } +export function applyPostSummonAbAttrs(attrType: { new(...args: any[]): PostSummonAbAttr }, + pokemon: Pokemon, ...args: any[]): void { + if (!pokemon.canApplyAbility()) + return; + + const ability = pokemon.getAbility(); + const attrs = ability.getAttrs(attrType) as PostSummonAbAttr[]; + for (let attr of attrs) { + if (!canApplyAttr(pokemon, attr)) + continue; + pokemon.scene.setPhaseQueueSplice(); + if (attr.applyPostSummon(pokemon, args)) { + queueShowAbility(pokemon); + const message = attr.getTriggerMessage(pokemon); + if (message) + pokemon.scene.queueMessage(message); + } + } + + pokemon.scene.clearPhaseQueueSplice(); +} + export function applyPreStatChangeAbAttrs(attrType: { new(...args: any[]): PreStatChangeAbAttr }, pokemon: Pokemon, stat: BattleStat, cancelled: Utils.BooleanHolder, ...args: any[]): void { if (!pokemon.canApplyAbility()) @@ -564,6 +752,50 @@ export function applyPreStatChangeAbAttrs(attrType: { new(...args: any[]): PreSt pokemon.scene.clearPhaseQueueSplice(); } +export function applyPreSetStatusAbAttrs(attrType: { new(...args: any[]): PreSetStatusAbAttr }, + pokemon: Pokemon, effect: StatusEffect, cancelled: Utils.BooleanHolder, ...args: any[]): void { + if (!pokemon.canApplyAbility()) + return; + + const ability = pokemon.getAbility(); + const attrs = ability.getAttrs(attrType) as PreSetStatusAbAttr[]; + for (let attr of attrs) { + if (!canApplyAttr(pokemon, attr)) + continue; + pokemon.scene.setPhaseQueueSplice(); + if (attr.applyPreSetStatus(pokemon, effect, cancelled, args)) { + queueShowAbility(pokemon); + const message = attr.getTriggerMessage(pokemon, effect); + if (message) + pokemon.scene.queueMessage(message); + } + } + + pokemon.scene.clearPhaseQueueSplice(); +} + +export function applyPreApplyBattlerTagAbAttrs(attrType: { new(...args: any[]): PreApplyBattlerTagAbAttr }, + pokemon: Pokemon, tag: BattlerTag, cancelled: Utils.BooleanHolder, ...args: any[]): void { + if (!pokemon.canApplyAbility()) + return; + + const ability = pokemon.getAbility(); + const attrs = ability.getAttrs(attrType) as PreApplyBattlerTagAbAttr[]; + for (let attr of attrs) { + if (!canApplyAttr(pokemon, attr)) + continue; + pokemon.scene.setPhaseQueueSplice(); + if (attr.applyPreApplyBattlerTag(pokemon, tag, cancelled, args)) { + queueShowAbility(pokemon); + const message = attr.getTriggerMessage(pokemon, tag); + if (message) + pokemon.scene.queueMessage(message); + } + } + + pokemon.scene.clearPhaseQueueSplice(); +} + export function applyPreWeatherEffectAbAttrs(attrType: { new(...args: any[]): PreWeatherEffectAbAttr }, pokemon: Pokemon, weather: Weather, cancelled: Utils.BooleanHolder, ...args: any[]): void { if (!pokemon.canApplyAbility()) @@ -635,6 +867,28 @@ export function applyPostWeatherLapseAbAttrs(attrType: { new(...args: any[]): Po pokemon.scene.clearPhaseQueueSplice(); } +export function applyCheckTrappedAbAttrs(attrType: { new(...args: any[]): CheckTrappedAbAttr }, + pokemon: Pokemon, trapped: Utils.BooleanHolder, ...args: any[]): void { + if (!pokemon.canApplyAbility()) + return; + + const ability = pokemon.getAbility(); + const attrs = ability.getAttrs(attrType) as CheckTrappedAbAttr[]; + for (let attr of attrs) { + if (!canApplyAttr(pokemon, attr)) + continue; + pokemon.scene.setPhaseQueueSplice(); + if (attr.applyCheckTrapped(pokemon, trapped, args)) { + // Don't show ability bar because this call is asynchronous + const message = attr.getTriggerMessage(pokemon); + if (message) + pokemon.scene.ui.showText(message, null, () => pokemon.scene.ui.showText(null, 0), null, true); + } + } + + pokemon.scene.clearPhaseQueueSplice(); +} + function canApplyAttr(pokemon: Pokemon, attr: AbAttr): boolean { const condition = attr.getCondition(); return !condition || condition(pokemon); @@ -841,12 +1095,16 @@ export function initAbilities() { new Ability(Abilities.COLOR_CHANGE, "Color Change (N)", "Changes the POKéMON's type to the foe's move.", 3), new Ability(Abilities.COMPOUND_EYES, "Compound Eyes", "The POKéMON's accuracy is boosted.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.3), - new Ability(Abilities.CUTE_CHARM, "Cute Charm (N)", "Contact with the POKéMON may cause infatuation.", 3), + new Ability(Abilities.CUTE_CHARM, "Cute Charm", "Contact with the POKéMON may cause infatuation.", 3) + .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), new Ability(Abilities.DAMP, "Damp (N)", "Prevents the use of self-destructing moves.", 3), - new Ability(Abilities.DRIZZLE, "Drizzle (N)", "The POKéMON makes it rain when it enters a battle.", 3), - new Ability(Abilities.DROUGHT, "Drought (N)", "Turns the sunlight harsh when the POKéMON enters a battle.", 3), + new Ability(Abilities.DRIZZLE, "Drizzle", "The POKéMON makes it rain when it enters a battle.", 3) + .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN), + new Ability(Abilities.DROUGHT, "Drought", "Turns the sunlight harsh when the POKéMON enters a battle.", 3) + .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY), new Ability(Abilities.EARLY_BIRD, "Early Bird (N)", "The POKéMON awakens quickly from sleep.", 3), - new Ability(Abilities.EFFECT_SPORE, "Effect Spore (N)", "Contact may poison or cause paralysis or sleep.", 3), + new Ability(Abilities.EFFECT_SPORE, "Effect Spore", "Contact may poison or cause paralysis or sleep.", 3) + .attr(PostDefendContactApplyStatusEffectAbAttr, 10, StatusEffect.POISON, StatusEffect.PARALYSIS, StatusEffect.SLEEP), new Ability(Abilities.FLAME_BODY, "Flame Body (N)", "Contact with the POKéMON may burn the attacker.", 3), new Ability(Abilities.FLASH_FIRE, "Flash Fire", "It powers up FIRE-type moves if it's hit by one.", 3) .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, 1, BattlerTagType.FIRE_BOOST, (pokemon: Pokemon) => !pokemon.status || pokemon.status.effect !== StatusEffect.FREEZE), @@ -857,9 +1115,12 @@ export function initAbilities() { new Ability(Abilities.HYPER_CUTTER, "Hyper Cutter", "Prevents other POKéMON from lowering ATTACK stat.", 3) .attr(ProtectStatAttr, BattleStat.ATK), new Ability(Abilities.ILLUMINATE, "Illuminate (N)", "Raises the likelihood of meeting wild POKéMON.", 3), - new Ability(Abilities.IMMUNITY, "Immunity (N)", "Prevents the POKéMON from getting poisoned.", 3), + new Ability(Abilities.IMMUNITY, "Immunity", "Prevents the POKéMON from getting poisoned.", 3) + .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON), new Ability(Abilities.INNER_FOCUS, "Inner Focus (N)", "The POKéMON is protected from flinching.", 3), - new Ability(Abilities.INSOMNIA, "Insomnia (N)", "Prevents the POKéMON from falling asleep.", 3), + new Ability(Abilities.INSOMNIA, "Insomnia", "Prevents the POKéMON from falling asleep.", 3) + .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) + .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY), new Ability(Abilities.INTIMIDATE, "Intimidate (N)", "Lowers the foe's ATTACK stat.", 3), new Ability(Abilities.KEEN_EYE, "Keen Eye", "Prevents other POKéMON from lowering accuracy.", 3) .attr(ProtectStatAttr, BattleStat.ACC), @@ -869,15 +1130,20 @@ export function initAbilities() { .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPATK, 1), new Ability(Abilities.LIMBER, "Limber (N)", "The POKéMON is protected from paralysis.", 3), new Ability(Abilities.LIQUID_OOZE, "Liquid Ooze (N)", "Damages attackers using any draining move.", 3), - new Ability(Abilities.MAGMA_ARMOR, "Magma Armor (N)", "Prevents the POKéMON from becoming frozen.", 3), - new Ability(Abilities.MAGNET_PULL, "Magnet Pull (N)", "Prevents STEEL-type POKéMON from escaping.", 3), + new Ability(Abilities.MAGMA_ARMOR, "Magma Armor", "Prevents the POKéMON from becoming frozen.", 3) + .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE), + new Ability(Abilities.MAGNET_PULL, "Magnet Pull", "Prevents STEEL-type POKéMON from escaping.", 3) + .attr(ArenaTrapAbAttr) + .condition((pokemon: Pokemon) => pokemon.getOpponent()?.isOfType(Type.STEEL)), new Ability(Abilities.MARVEL_SCALE, "Marvel Scale (N)", "Ups DEFENSE if there is a status problem.", 3), new Ability(Abilities.MINUS, "Minus (N)", "Ups SP. ATK if another POKéMON has PLUS or MINUS.", 3), new Ability(Abilities.NATURAL_CURE, "Natural Cure (N)", "All status problems heal when it switches out.", 3), - new Ability(Abilities.OBLIVIOUS, "Oblivious (N)", "Prevents it from becoming infatuated.", 3), + new Ability(Abilities.OBLIVIOUS, "Oblivious", "Prevents it from becoming infatuated.", 3) + .attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED), new Ability(Abilities.OVERGROW, "Overgrow", "Powers up GRASS-type moves in a pinch.", 3) .attr(LowHpMoveTypePowerBoostAbAttr, Type.GRASS), - new Ability(Abilities.OWN_TEMPO, "Own Tempo (N)", "Prevents the POKéMON from becoming confused.", 3), + new Ability(Abilities.OWN_TEMPO, "Own Tempo", "Prevents the POKéMON from becoming confused.", 3) + .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED), new Ability(Abilities.PICKUP, "Pickup (N)", "The POKéMON may pick up items.", 3), new Ability(Abilities.PLUS, "Plus (N)", "Ups SP. ATK if another POKéMON has PLUS or MINUS.", 3), new Ability(Abilities.POISON_POINT, "Poison Point (N)", "Contact with the POKéMON may poison the attacker.", 3), @@ -889,7 +1155,8 @@ export function initAbilities() { .attr(BlockRecoilDamageAttr), new Ability(Abilities.ROUGH_SKIN, "Rough Skin (N)", "Inflicts damage to the attacker on contact.", 3), new Ability(Abilities.RUN_AWAY, "Run Away (N)", "Enables a sure getaway from wild POKéMON.", 3), - new Ability(Abilities.SAND_STREAM, "Sand Stream (N)", "The POKéMON summons a sandstorm in battle.", 3), + new Ability(Abilities.SAND_STREAM, "Sand Stream", "The POKéMON summons a sandstorm in battle.", 3) + .attr(PostSummonWeatherChangeAbAttr, WeatherType.SANDSTORM), new Ability(Abilities.SAND_VEIL, "Sand Veil", "Boosts the POKéMON's evasion in a sandstorm.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) @@ -920,12 +1187,15 @@ export function initAbilities() { .attr(LowHpMoveTypePowerBoostAbAttr, Type.WATER), new Ability(Abilities.TRACE, "Trace (N)", "The POKéMON copies a foe's Ability.", 3), new Ability(Abilities.TRUANT, "Truant (N)", "POKéMON can't attack on consecutive turns.", 3), - new Ability(Abilities.VITAL_SPIRIT, "Vital Spirit (N)", "Prevents the POKéMON from falling asleep.", 3), + new Ability(Abilities.VITAL_SPIRIT, "Vital Spirit", "Prevents the POKéMON from falling asleep.", 3) + .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) + .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY), new Ability(Abilities.VOLT_ABSORB, "Volt Absorb", "Restores HP if hit by an ELECTRIC-type move.", 3) .attr(TypeImmunityHealAbAttr, Type.ELECTRIC), new Ability(Abilities.WATER_ABSORB, "Water Absorb", "Restores HP if hit by a WATER-type move.", 3) .attr(TypeImmunityHealAbAttr, Type.WATER), - new Ability(Abilities.WATER_VEIL, "Water Veil (N)", "Prevents the POKéMON from getting a burn.", 3), + new Ability(Abilities.WATER_VEIL, "Water Veil", "Prevents the POKéMON from getting a burn.", 3) + .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN), new Ability(Abilities.WHITE_SMOKE, "White Smoke", "Prevents other POKéMON from lowering its stats.", 3) .attr(ProtectStatAttr), new Ability(Abilities.WONDER_GUARD, "Wonder Guard", "Only super effective moves will hit.", 3) @@ -953,7 +1223,9 @@ export function initAbilities() { .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL), new Ability(Abilities.IRON_FIST, "Iron Fist (N)", "Boosts the power of punching moves.", 4), new Ability(Abilities.KLUTZ, "Klutz (N)", "The POKéMON can't use any held items.", 4), - new Ability(Abilities.LEAF_GUARD, "Leaf Guard (N)", "Prevents problems with status in sunny weather.", 4), + new Ability(Abilities.LEAF_GUARD, "Leaf Guard", "Prevents problems with status in sunny weather.", 4) + .attr(StatusEffectImmunityAbAttr) + .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), new Ability(Abilities.MAGIC_GUARD, "Magic Guard (N)", "Protects the POKéMON from indirect damage.", 4), new Ability(Abilities.MOLD_BREAKER, "Mold Breaker (N)", "Moves can be used regardless of Abilities.", 4), new Ability(Abilities.MOTOR_DRIVE, "Motor Drive", "Raises SPEED if hit by an ELECTRIC-type move.", 4) @@ -972,7 +1244,8 @@ export function initAbilities() { new Ability(Abilities.SLOW_START, "Slow Start (N)", "Temporarily halves ATTACK and SPEED.", 4), new Ability(Abilities.SNIPER, "Sniper (N)", "Powers up moves if they become critical hits.", 4), new Ability(Abilities.SNOW_CLOAK, "Snow Cloak (N)", "Raises evasion in a hailstorm.", 4), - new Ability(Abilities.SNOW_WARNING, "Snow Warning (N)", "The POKéMON summons a hailstorm in battle.", 4), + new Ability(Abilities.SNOW_WARNING, "Snow Warning", "The POKéMON summons a hailstorm in battle.", 4) + .attr(PostSummonWeatherChangeAbAttr, WeatherType.HAIL), new Ability(Abilities.SOLAR_POWER, "Solar Power (N)", "In sunshine, SP. ATK is boosted but HP decreases.", 4), new Ability(Abilities.SOLID_ROCK, "Solid Rock (N)", "Reduces damage from super-effective attacks.", 4), new Ability(Abilities.STALL, "Stall (N)", "The POKéMON moves after all other POKéMON do.", 4), diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ee5a2584639..a0124e426e0 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -134,7 +134,7 @@ class SpikesTag extends ArenaTrapTag { super.onAdd(arena); const source = arena.scene.getPokemonById(this.sourceId); - const target = source.isPlayer() ? source.scene.getEnemyPokemon() : source.scene.getPlayerPokemon(); + const target = source.getOpponent(); arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${target.name}'s feet!`); } @@ -161,7 +161,7 @@ class ToxicSpikesTag extends ArenaTrapTag { super.onAdd(arena); const source = arena.scene.getPokemonById(this.sourceId); - const target = source.isPlayer() ? source.scene.getEnemyPokemon() : source.scene.getPlayerPokemon(); + const target = source.getOpponent(); arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${target.name}'s feet!`); } @@ -187,7 +187,7 @@ class StealthRockTag extends ArenaTrapTag { super.onAdd(arena); const source = arena.scene.getPokemonById(this.sourceId); - const target = source.isPlayer() ? source.scene.getEnemyPokemon() : source.scene.getPlayerPokemon(); + const target = source.getOpponent(); arena.scene.queueMessage(`Pointed stones float in the air\naround ${target.name}!`); } diff --git a/src/data/battler-tag.ts b/src/data/battler-tag.ts index 0db40ffd476..665bc90c180 100644 --- a/src/data/battler-tag.ts +++ b/src/data/battler-tag.ts @@ -7,12 +7,14 @@ import { StatusEffect } from "./status-effect"; import * as Utils from "../utils"; import { Moves, allMoves } from "./move"; import { Type } from "./type"; +import { Gender } from "./gender"; export enum BattlerTagType { NONE, RECHARGING, FLINCHED, CONFUSED, + INFATUATED, SEEDED, NIGHTMARE, FRENZY, @@ -77,6 +79,10 @@ export class BattlerTag { return --this.turnCount > 0; } + getDescriptor(): string { + return ''; + } + isSourceLinked(): boolean { return false; } @@ -130,6 +136,10 @@ export class TrappedTag extends BattlerTag { pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` was freed\nfrom ${this.getMoveName()}!`)); } + getDescriptor(): string { + return 'trapping'; + } + isSourceLinked(): boolean { return true; } @@ -152,6 +162,10 @@ export class FlinchedTag extends BattlerTag { return true; } + + getDescriptor(): string { + return 'flinching'; + } } export class ConfusedTag extends BattlerTag { @@ -198,6 +212,58 @@ export class ConfusedTag extends BattlerTag { return ret; } + + getDescriptor(): string { + return 'confusion'; + } +} + +export class InfatuatedTag extends BattlerTag { + constructor(sourceMove: integer, sourceId: integer) { + super(BattlerTagType.INFATUATED, BattlerTagLapseType.MOVE, 1, sourceMove, sourceId); + } + + canAdd(pokemon: Pokemon): boolean { + return pokemon.isOppositeGender(pokemon.scene.getPokemonById(this.sourceId)); + } + + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` fell in love\nwith ${pokemon.scene.getPokemonById(this.sourceId).name}!`)); + } + + onOverlap(pokemon: Pokemon): void { + super.onOverlap(pokemon); + + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nalready in love!')); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); + + if (ret) { + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` is in love\nwith ${pokemon.scene.getPokemonById(this.sourceId).name}!`)); + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.ATTRACT)); + + if (Utils.randInt(2)) { + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nimmobilized by love!')); + (pokemon.scene.getCurrentPhase() as MovePhase).cancel(); + } + } + + return ret; + } + + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + + pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' got over\nits infatuation.')); + } + + getDescriptor(): string { + return 'infatuation'; + } } export class SeedTag extends BattlerTag { @@ -225,6 +291,10 @@ export class SeedTag extends BattlerTag { return ret; } + + getDescriptor(): string { + return 'seeding'; + } } export class NightmareTag extends BattlerTag { @@ -258,6 +328,10 @@ export class NightmareTag extends BattlerTag { return ret; } + + getDescriptor(): string { + return 'nightmares'; + } } export class IngrainTag extends TrappedTag { @@ -278,6 +352,10 @@ export class IngrainTag extends TrappedTag { getTrapMessage(pokemon: Pokemon): string { return getPokemonMessage(pokemon, ' planted its roots!'); } + + getDescriptor(): string { + return 'roots'; + } } export class AquaRingTag extends BattlerTag { @@ -320,6 +398,10 @@ export class DrowsyTag extends BattlerTag { return true; } + + getDescriptor(): string { + return 'drowsiness'; + } } export abstract class DamagingTrapTag extends TrappedTag { @@ -357,7 +439,7 @@ export class BindTag extends DamagingTrapTag { } getTrapMessage(pokemon: Pokemon): string { - return getPokemonMessage(pokemon, ` was squeezed by\n${pokemon.scene.getPokemonById(this.sourceId)}'s ${this.getMoveName()}!`); + return getPokemonMessage(pokemon, ` was squeezed by\n${pokemon.scene.getPokemonById(this.sourceId).name}'s ${this.getMoveName()}!`); } } @@ -367,7 +449,7 @@ export class WrapTag extends DamagingTrapTag { } getTrapMessage(pokemon: Pokemon): string { - return getPokemonMessage(pokemon, ` was WRAPPED\nby ${pokemon.scene.getPokemonById(this.sourceId)}!`); + return getPokemonMessage(pokemon, ` was WRAPPED\nby ${pokemon.scene.getPokemonById(this.sourceId).name}!`); } } @@ -516,6 +598,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc return new FlinchedTag(sourceMove); case BattlerTagType.CONFUSED: return new ConfusedTag(turnCount, sourceMove); + case BattlerTagType.INFATUATED: + return new InfatuatedTag(sourceMove, sourceId); case BattlerTagType.SEEDED: return new SeedTag(sourceId); case BattlerTagType.NIGHTMARE: diff --git a/src/data/berry.ts b/src/data/berry.ts index 0494fbc6251..b5f2fc30aad 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -55,7 +55,7 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { return (pokemon: Pokemon) => !!pokemon.status || !!pokemon.getTag(BattlerTagType.CONFUSED); case BerryType.ENIGMA: return (pokemon: Pokemon) => { - const opponent = pokemon.isPlayer() ? pokemon.scene.getEnemyPokemon() : pokemon.scene.getPlayerPokemon(); + const opponent = pokemon.getOpponent(); const opponentLastMove = opponent ? opponent.getLastXMoves(1).find(() => true) : null; // TODO: Update so this works even if opponent has fainted return opponentLastMove && opponentLastMove.turn === pokemon.scene.currentBattle?.turn - 1 && opponentLastMove.result === MoveResult.SUPER_EFFECTIVE; diff --git a/src/data/move.ts b/src/data/move.ts index 61a854e6e4d..637b3e17015 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -11,6 +11,7 @@ import { WeatherType } from "./weather"; import { ArenaTagType, ArenaTrapTag } from "./arena-tag"; import { BlockRecoilDamageAttr, applyAbAttrs } from "./ability"; import { PokemonHeldItemModifier } from "../modifier/modifier"; +import { Gender } from "./gender"; export enum MoveCategory { PHYSICAL, @@ -1868,7 +1869,7 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon export function getMoveTarget(user: Pokemon, move: Moves): Pokemon { const moveTarget = allMoves[move].moveTarget; - const other = user.isPlayer() ? user.scene.getEnemyPokemon() : user.scene.getPlayerPokemon(); + const other = user.getOpponent(); switch (moveTarget) { case MoveTarget.USER: @@ -2364,7 +2365,9 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.DEF, 1, true), new StatusMove(Moves.MEAN_LOOK, "Mean Look", Type.NORMAL, -1, 5, -1, "Opponent cannot flee or switch.", -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, 1, true), - new StatusMove(Moves.ATTRACT, "Attract (N)", Type.NORMAL, 100, 15, -1, "If opponent is the opposite gender, it's less likely to attack.", -1, 0, 2), + new StatusMove(Moves.ATTRACT, "Attract", Type.NORMAL, 100, 15, -1, "If opponent is the opposite gender, it's less likely to attack.", -1, 0, 2) + .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) + .condition((user: Pokemon, target: Pokemon, move: Move) => user.isOppositeGender(target)), new SelfStatusMove(Moves.SLEEP_TALK, "Sleep Talk", Type.NORMAL, -1, 10, 70, "User performs one of its own moves while sleeping.", -1, 0, 2) .attr(BypassSleepAttr) .attr(RandomMovesetMoveAttr) diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index bf6d7a0ff4f..9769f79d1eb 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1009,7 +1009,7 @@ export class HeldItemTransferModifier extends PokemonHeldItemModifier { apply(args: any[]): boolean { const pokemon = args[0] as Pokemon; - const targetPokemon = pokemon.isPlayer() ? pokemon.scene.getEnemyPokemon() : pokemon.scene.getPlayerPokemon(); + const targetPokemon = pokemon.getOpponent(); if (!targetPokemon) return false; diff --git a/src/pokemon.ts b/src/pokemon.ts index 595c6e30d4e..7febdf67039 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -23,7 +23,7 @@ import { WeatherType } from './data/weather'; import { TempBattleStat } from './data/temp-battle-stat'; import { WeakenMoveTypeTag } from './data/arena-tag'; import { Biome } from './data/biome'; -import { Abilities, Ability, BattleStatMultiplierAbAttr, BlockCritAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyBattleStatMultiplierAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs } from './data/ability'; +import { Abilities, Ability, BattleStatMultiplierAbAttr, BlockCritAbAttr, PreApplyBattlerTagAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability'; import PokemonData from './system/pokemon-data'; export default abstract class Pokemon extends Phaser.GameObjects.Container { @@ -483,6 +483,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.levelExp = this.exp - getLevelTotalExp(this.level, this.getSpeciesForm().growthRate); } + getOpponent(): Pokemon { + const ret = this.isPlayer() ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon(); + if (ret.summonData) + return ret; + return null; + } + apply(source: Pokemon, battlerMove: PokemonMove): MoveResult { let result: MoveResult; const move = battlerMove.getMove(); @@ -600,7 +607,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!this.hp) { this.scene.pushPhase(new FaintPhase(this.scene, this.isPlayer())); this.resetSummonData(); - (this.isPlayer() ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon()).resetBattleSummonData(); + this.getOpponent()?.resetBattleSummonData(); } } @@ -613,7 +620,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const newTag = getBattlerTag(tagType, turnCount || 0, sourceMove, sourceId); - if (newTag.canAdd(this)) { + const cancelled = new Utils.BooleanHolder(false); + applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled); + + if (!cancelled.value && newTag.canAdd(this)) { this.summonData.tags.push(newTag); newTag.onAdd(this); @@ -761,6 +771,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } + isOppositeGender(pokemon: Pokemon): boolean { + return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE); + } + trySetStatus(effect: StatusEffect): boolean { if (this.status) return false; @@ -779,6 +793,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; break; } + + const cancelled = new Utils.BooleanHolder(false); + applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled); + + if (cancelled.value) + return false; + this.status = new Status(effect); return true; }