diff --git a/src/data/ability.ts b/src/data/ability.ts index 0286d087093..eca749b030a 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3786,6 +3786,54 @@ export class IceFaceBlockPhysicalAbAttr extends ReceivedMoveDamageMultiplierAbAt } } +/** + * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). + * + * @extends AbAttr + */ +export class BypassSpeedChanceAbAttr extends AbAttr { + public chance: integer; + + /** + * @param {integer} chance probability of ability being active. + */ + constructor(chance: integer) { + super(true); + this.chance = chance; + } + + /** + * bypass move order in their priority bracket when pokemon choose damaging move + * @param {Pokemon} pokemon {@linkcode Pokemon} the Pokemon applying this ability + * @param {boolean} passive N/A + * @param {Utils.BooleanHolder} cancelled N/A + * @param {any[]} args [0] {@linkcode Utils.BooleanHolder} set to true when the ability activated + * @returns {boolean} - whether the ability was activated. + */ + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + const bypassSpeed = args[0] as Utils.BooleanHolder; + + if (!bypassSpeed.value && pokemon.randSeedInt(100) < this.chance) { + const turnCommand = + pokemon.scene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; + const isCommandFight = turnCommand?.command === Command.FIGHT; + const move = allMoves[turnCommand.move?.move]; + const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; + + if (isCommandFight && isDamageMove) { + bypassSpeed.value = true; + return true; + } + } + + return false; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return i18next.t("abilityTriggers:quickDraw", {pokemonName: getPokemonNameWithAffix(pokemon)}); + } +} + function applyAbAttrsInternal(attrType: Constructor, pokemon: Pokemon, applyFunc: AbAttrApplyFunc, args: any[], isAsync: boolean = false, showAbilityInstant: boolean = false, quiet: boolean = false, passive: boolean = false): Promise { return new Promise(resolve => { @@ -4870,7 +4918,7 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .condition((pokemon) => !pokemon.isTerastallized()), new Ability(Abilities.QUICK_DRAW, 8) - .unimplemented(), + .attr(BypassSpeedChanceAbAttr, 30), new Ability(Abilities.UNSEEN_FIST, 8) .attr(IgnoreProtectOnContactAbAttr), new Ability(Abilities.CURIOUS_MEDICINE, 8) diff --git a/src/locales/de/ability-trigger.ts b/src/locales/de/ability-trigger.ts index 31bf3a48517..d6c48555d51 100644 --- a/src/locales/de/ability-trigger.ts +++ b/src/locales/de/ability-trigger.ts @@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "iceFaceAvoidedDamage": "{{pokemonName}} wehrt Schaden mit {{abilityName}} ab!", "trace": "{{pokemonName}} kopiert {{abilityName}} von {{targetName}}!", "windPowerCharged": "Der Treffer durch {{moveName}} läd die Stärke von {{pokemonName}} auf!", + "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/en/ability-trigger.ts b/src/locales/en/ability-trigger.ts index 5852eb6315b..b516bc8dde0 100644 --- a/src/locales/en/ability-trigger.ts +++ b/src/locales/en/ability-trigger.ts @@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "poisonHeal": "{{pokemonName}}'s {{abilityName}}\nrestored its HP a little!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!", + "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/es/ability-trigger.ts b/src/locales/es/ability-trigger.ts index 97153617f1e..5c09c3832c0 100644 --- a/src/locales/es/ability-trigger.ts +++ b/src/locales/es/ability-trigger.ts @@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "iceFaceAvoidedDamage": "¡{{pokemonNameWithAffix}} evitó\ndaño con {{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!", + "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/fr/ability-trigger.ts b/src/locales/fr/ability-trigger.ts index 4cc742a93ee..f99ff15c26f 100644 --- a/src/locales/fr/ability-trigger.ts +++ b/src/locales/fr/ability-trigger.ts @@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaure un peu ses PV !", "trace": "{{pokemonName}} copie le talent {{abilityName}}\nde {{targetName}} !", "windPowerCharged": "{{pokemonName}} a été touché par la capacité {{moveName}} et se charge en électricité !", + "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/it/ability-trigger.ts b/src/locales/it/ability-trigger.ts index 29625456c49..1f6fcfb1258 100644 --- a/src/locales/it/ability-trigger.ts +++ b/src/locales/it/ability-trigger.ts @@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "iceFaceAvoidedDamage": "{{pokemonName}} ha evitato\ni danni grazie a {{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "Venire colpito da {{moveName}} ha caricato {{pokemonName}}!", + "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/ko/ability-trigger.ts b/src/locales/ko/ability-trigger.ts index 2fb40220c36..7bc38977278 100644 --- a/src/locales/ko/ability-trigger.ts +++ b/src/locales/ko/ability-trigger.ts @@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "poisonHeal": "{{pokemonName}}[[는]] {{abilityName}}[[로]]인해\n조금 회복했다.", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "{{pokemonName}}[[는]]\n{{moveName}}에 맞아 충전되었다!", + "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/pt_BR/ability-trigger.ts b/src/locales/pt_BR/ability-trigger.ts index e6140f8cf8f..4e3d6d11487 100644 --- a/src/locales/pt_BR/ability-trigger.ts +++ b/src/locales/pt_BR/ability-trigger.ts @@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaurou seus PS um pouco!", "trace": "{{pokemonName}} copiou {{abilityName}}\nde {{targetName}}!", "windPowerCharged": "Ser atingido por {{moveName}} carregou {{pokemonName}} com poder!", + "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/zh_CN/ability-trigger.ts b/src/locales/zh_CN/ability-trigger.ts index 7941433cdf1..9f128e41ad1 100644 --- a/src/locales/zh_CN/ability-trigger.ts +++ b/src/locales/zh_CN/ability-trigger.ts @@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "iceFaceAvoidedDamage": "{{pokemonName}} 因为 {{abilityName}}\n避免了伤害!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "受 {{moveName}} 的影响, {{pokemonName}} 提升了能力!", + "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/locales/zh_TW/ability-trigger.ts b/src/locales/zh_TW/ability-trigger.ts index 24476c2c76e..c436e5021f7 100644 --- a/src/locales/zh_TW/ability-trigger.ts +++ b/src/locales/zh_TW/ability-trigger.ts @@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = { "iceFaceAvoidedDamage": "{{pokemonName}} 因爲 {{abilityName}}\n避免了傷害!", "trace": "{{pokemonName}} 複製了 {{targetName}} 的\n{{abilityName}}!", "windPowerCharged": "受 {{moveName}} 的影響, {{pokemonName}} 提升了能力!", + "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", } as const; diff --git a/src/phases.ts b/src/phases.ts index fcc1051622f..0d86a98ef92 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -26,7 +26,7 @@ import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -2239,6 +2239,7 @@ export class TurnStartPhase extends FieldPhase { this.scene.getField(true).filter(p => p.summonData).map(p => { const bypassSpeed = new Utils.BooleanHolder(false); + applyAbAttrs(BypassSpeedChanceAbAttr, p, null, bypassSpeed); this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; }); diff --git a/src/test/abilities/quick_draw.test.ts b/src/test/abilities/quick_draw.test.ts new file mode 100644 index 00000000000..6b72b83bd18 --- /dev/null +++ b/src/test/abilities/quick_draw.test.ts @@ -0,0 +1,117 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import {Abilities} from "#enums/abilities"; +import {Species} from "#enums/species"; +import {EnemyCommandPhase, TitlePhase, TurnEndPhase, TurnStartPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { Stat } from "#app/data/pokemon-stat"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { allAbilities, BypassSpeedChanceAbAttr } from "#app/data/ability"; + + +describe("Abilities - Quick Draw", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue( + Abilities.QUICK_DRAW + ); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue( + Species.RATTATA + ); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ + Moves.TACKLE, + Moves.TACKLE, + Moves.TACKLE, + Moves.TACKLE, + ]); + + vi.spyOn( + allAbilities[Abilities.QUICK_DRAW].getAttrs(BypassSpeedChanceAbAttr)[0], + "chance","get" + ).mockReturnValue(100); + }); + + it("makes pokemon going first in its priority bracket", async() => { + await game.startBattle([Species.SLOWBRO]); + + const pokemon = game.scene.getParty()[0]; + const enemy = game.scene.getEnemyParty()[0]; + + pokemon.stats[Stat.SPD] = 50; + enemy.stats[Stat.SPD] = 150; + pokemon.hp = 1; + enemy.hp = 1; + + game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); + + await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.run(TurnStartPhase); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(pokemon.battleData.abilityRevealed).toBe(true); + }, 20000); + + it("does not triggered by non damage moves", async () => { + await game.startBattle([Species.SLOWBRO]); + + const pokemon = game.scene.getParty()[0]; + const enemy = game.scene.getEnemyParty()[0]; + + pokemon.stats[Stat.SPD] = 50; + enemy.stats[Stat.SPD] = 150; + pokemon.hp = 1; + enemy.hp = 1; + + game.doAttack(getMovePosition(game.scene, 0, Moves.TOXIC)); + + await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.run(TurnStartPhase); + await game.phaseInterceptor.to(TitlePhase); + + expect(pokemon.battleData.abilityRevealed).not.toBe(true); + }, 20000); + + it("does not increase priority", async () => { + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ + Moves.EXTREME_SPEED, + Moves.EXTREME_SPEED, + Moves.EXTREME_SPEED, + Moves.EXTREME_SPEED, + ]); + + await game.startBattle([Species.SLOWBRO]); + + const pokemon = game.scene.getParty()[0]; + const enemy = game.scene.getEnemyParty()[0]; + + pokemon.stats[Stat.SPD] = 50; + enemy.stats[Stat.SPD] = 150; + pokemon.hp = 1; + enemy.hp = 1; + + game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE)); + + await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.run(TurnStartPhase); + await game.phaseInterceptor.to(TitlePhase); + + expect(pokemon.battleData.abilityRevealed).toBe(true); + }, 20000); +});