diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 66bcc7b9c3c..67abb9d9ba8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1863,6 +1863,30 @@ export class ExposedTag extends BattlerTag { } } +/** + * Tag that doubles the type effectiveness of a move. + * @extends BattlerTag + */ +export class DoubleMoveTypeEffectivenessTag extends BattlerTag { + /** The move's type effectiveness to be doubled. */ + public moveType: Type; + + constructor(moveType: Type, tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) { + super(tagType, lapseType, 1, sourceMove); + + this.moveType = moveType; + } + + /** + * If the Pokemon is terastallized, the tag cannot be added. + * @param pokemon the pokemon to which the tag is added + * @returns whether the tag is applied + */ + canAdd(pokemon: Pokemon): boolean { + return !pokemon.isTerastallized(); + } +} + export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag { switch (tagType) { @@ -2002,6 +2026,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.GULP_MISSILE_ARROKUDA: case BattlerTagType.GULP_MISSILE_PIKACHU: return new GulpMissileTag(tagType, sourceMove); + case BattlerTagType.TAR_SHOT: + return new DoubleMoveTypeEffectivenessTag(Type.FIRE, tagType, BattlerTagLapseType.CUSTOM, sourceMove); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index ddf043c554d..a8f98a0ca04 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8692,7 +8692,7 @@ export function initMoves() { .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false), new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) .attr(ChangeTypeAttr, Type.PSYCHIC) .powderMove(), diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 20ceb1b331f..88b3070fe15 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -72,4 +72,5 @@ export enum BattlerTagType { SHELL_TRAP = "SHELL_TRAP", DRAGON_CHEER = "DRAGON_CHEER", NO_RETREAT = "NO_RETREAT", + TAR_SHOT = "TAR_SHOT", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e0a9a4a86ce..ae4d015454d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, DoubleMoveTypeEffectivenessTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, 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 } from "../data/ability"; @@ -1348,7 +1348,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the effectiveness of a move against the Pokémon. - * + * This includes modifiers from move and ability attributes. * @param source {@linkcode Pokemon} The attacking Pokémon. * @param move {@linkcode Move} The move being used by the attacking Pokémon. * @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). @@ -1372,6 +1372,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { typeMultiplier.value = 0; } + const doubleMoveTypeEffectiveness = this.getTag(DoubleMoveTypeEffectivenessTag); + if (doubleMoveTypeEffectiveness && doubleMoveTypeEffectiveness.moveType === this.getMoveType(move)) { + typeMultiplier.value *= 2; + } + const cancelledHolder = cancelled ?? new Utils.BooleanHolder(false); if (!ignoreAbility) { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier); @@ -1403,7 +1408,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the type effectiveness multiplier for an attack type + * Calculates the move's type effectiveness multiplier based on the target's type/s. * @param moveType {@linkcode Type} the type of the move being used * @param source {@linkcode Pokemon} the Pokemon using the move * @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks) diff --git a/src/test/moves/tar_shot.test.ts b/src/test/moves/tar_shot.test.ts new file mode 100644 index 00000000000..03aed78bef2 --- /dev/null +++ b/src/test/moves/tar_shot.test.ts @@ -0,0 +1,88 @@ +import { BattlerIndex } from "#app/battle.js"; +import { Moves } from "#app/enums/moves.js"; +import { Species } from "#app/enums/species.js"; +import { Stat } from "#app/enums/stat.js"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Tar Shot", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY) + .enemySpecies(Species.TANGELA) + .enemyLevel(10) + .moveset([Moves.TAR_SHOT, Moves.FIRE_PUNCH]) + .disableCrits(); + }); + + it("lowers the target's Speed stat by one stage and doubles the effectiveness of Fire-type moves used on the target", async () => { + await game.classicMode.startBattle([Species.PIKACHU]); + + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMaxHp").mockReturnValue(1000); + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.TAR_SHOT); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getStatStage(Stat.SPD)).toBe(-1); + + await game.toNextTurn(); + + game.move.select(Moves.FIRE_PUNCH); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); + }, TIMEOUT); + + it("will not double the effectiveness of Fire-type moves used on a target that is already under the effect of Tar Shot (but may still lower its Speed)", async () => { + await game.classicMode.startBattle([Species.PIKACHU]); + + const enemy = game.scene.getEnemyPokemon()!; + + vi.spyOn(enemy, "getMaxHp").mockReturnValue(1000); + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.TAR_SHOT); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getStatStage(Stat.SPD)).toBe(-1); + + await game.toNextTurn(); + + game.move.select(Moves.TAR_SHOT); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getStatStage(Stat.SPD)).toBe(-2); + + await game.toNextTurn(); + + game.move.select(Moves.FIRE_PUNCH); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); + }, TIMEOUT); +});