[Ability] Implement Quick Draw (#2287)
* implement quick draw * add line * change ability success condition * quick draw comes before quick claw effect * now ability has chance variable to test conditional situation * add unit test * Merge branch 'main' into ability-quick-draw * modifed: ability.getTriggerMessage * modified: to use pokemon.battleData.abilityRevealed * modified: changed private variable to public * erase expect interceptor log * add localizations * modified: add comma in locales * modified: ability docs & trigger logic * modified: changed test logic
This commit is contained in:
parent
e9fb13cce9
commit
8c8ddd26b5
|
@ -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<TAttr extends AbAttr>(attrType: Constructor<TAttr>,
|
||||
pokemon: Pokemon, applyFunc: AbAttrApplyFunc<TAttr>, args: any[], isAsync: boolean = false, showAbilityInstant: boolean = false, quiet: boolean = false, passive: boolean = false): Promise<void> {
|
||||
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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
Loading…
Reference in New Issue