[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:
cadi 2024-06-25 00:22:15 +09:00 committed by GitHub
parent e9fb13cce9
commit 8c8ddd26b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 177 additions and 2 deletions

View File

@ -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>, 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> { 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 => { return new Promise(resolve => {
@ -4870,7 +4918,7 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.condition((pokemon) => !pokemon.isTerastallized()), .condition((pokemon) => !pokemon.isTerastallized()),
new Ability(Abilities.QUICK_DRAW, 8) new Ability(Abilities.QUICK_DRAW, 8)
.unimplemented(), .attr(BypassSpeedChanceAbAttr, 30),
new Ability(Abilities.UNSEEN_FIST, 8) new Ability(Abilities.UNSEEN_FIST, 8)
.attr(IgnoreProtectOnContactAbAttr), .attr(IgnoreProtectOnContactAbAttr),
new Ability(Abilities.CURIOUS_MEDICINE, 8) new Ability(Abilities.CURIOUS_MEDICINE, 8)

View File

@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"iceFaceAvoidedDamage": "{{pokemonName}} wehrt Schaden mit {{abilityName}} ab!", "iceFaceAvoidedDamage": "{{pokemonName}} wehrt Schaden mit {{abilityName}} ab!",
"trace": "{{pokemonName}} kopiert {{abilityName}} von {{targetName}}!", "trace": "{{pokemonName}} kopiert {{abilityName}} von {{targetName}}!",
"windPowerCharged": "Der Treffer durch {{moveName}} läd die Stärke von {{pokemonName}} auf!", "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; } as const;

View File

@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"poisonHeal": "{{pokemonName}}'s {{abilityName}}\nrestored its HP a little!", "poisonHeal": "{{pokemonName}}'s {{abilityName}}\nrestored its HP a little!",
"trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!",
"windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!", "windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!",
"quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"iceFaceAvoidedDamage": "¡{{pokemonNameWithAffix}} evitó\ndaño con {{abilityName}}!", "iceFaceAvoidedDamage": "¡{{pokemonNameWithAffix}} evitó\ndaño con {{abilityName}}!",
"trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!",
"windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!", "windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!",
"quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaure un peu ses PV !", "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaure un peu ses PV !",
"trace": "{{pokemonName}} copie le talent {{abilityName}}\nde {{targetName}} !", "trace": "{{pokemonName}} copie le talent {{abilityName}}\nde {{targetName}} !",
"windPowerCharged": "{{pokemonName}} a été touché par la capacité {{moveName}} et se charge en électricité !", "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; } as const;

View File

@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"iceFaceAvoidedDamage": "{{pokemonName}} ha evitato\ni danni grazie a {{abilityName}}!", "iceFaceAvoidedDamage": "{{pokemonName}} ha evitato\ni danni grazie a {{abilityName}}!",
"trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!",
"windPowerCharged": "Venire colpito da {{moveName}} ha caricato {{pokemonName}}!", "windPowerCharged": "Venire colpito da {{moveName}} ha caricato {{pokemonName}}!",
"quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"poisonHeal": "{{pokemonName}}[[는]] {{abilityName}}[[로]]인해\n조금 회복했다.", "poisonHeal": "{{pokemonName}}[[는]] {{abilityName}}[[로]]인해\n조금 회복했다.",
"trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!",
"windPowerCharged": "{{pokemonName}}[[는]]\n{{moveName}}에 맞아 충전되었다!", "windPowerCharged": "{{pokemonName}}[[는]]\n{{moveName}}에 맞아 충전되었다!",
"quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -9,4 +9,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaurou seus PS um pouco!", "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaurou seus PS um pouco!",
"trace": "{{pokemonName}} copiou {{abilityName}}\nde {{targetName}}!", "trace": "{{pokemonName}} copiou {{abilityName}}\nde {{targetName}}!",
"windPowerCharged": "Ser atingido por {{moveName}} carregou {{pokemonName}} com poder!", "windPowerCharged": "Ser atingido por {{moveName}} carregou {{pokemonName}} com poder!",
"quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"iceFaceAvoidedDamage": "{{pokemonName}} 因为 {{abilityName}}\n避免了伤害", "iceFaceAvoidedDamage": "{{pokemonName}} 因为 {{abilityName}}\n避免了伤害",
"trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!",
"windPowerCharged": "受 {{moveName}} 的影响, {{pokemonName}} 提升了能力!", "windPowerCharged": "受 {{moveName}} 的影响, {{pokemonName}} 提升了能力!",
"quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -7,4 +7,5 @@ export const abilityTriggers: SimpleTranslationEntries = {
"iceFaceAvoidedDamage": "{{pokemonName}} 因爲 {{abilityName}}\n避免了傷害", "iceFaceAvoidedDamage": "{{pokemonName}} 因爲 {{abilityName}}\n避免了傷害",
"trace": "{{pokemonName}} 複製了 {{targetName}} 的\n{{abilityName}}!", "trace": "{{pokemonName}} 複製了 {{targetName}} 的\n{{abilityName}}!",
"windPowerCharged": "受 {{moveName}} 的影響, {{pokemonName}} 提升了能力!", "windPowerCharged": "受 {{moveName}} 的影響, {{pokemonName}} 提升了能力!",
"quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",
} as const; } as const;

View File

@ -26,7 +26,7 @@ import { Gender } from "./data/gender";
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
import { TempBattleStat } from "./data/temp-battle-stat"; import { TempBattleStat } from "./data/temp-battle-stat";
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; 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 { Unlockables, getUnlockableName } from "./system/unlockables";
import { getBiomeKey } from "./field/arena"; import { getBiomeKey } from "./field/arena";
import { BattleType, BattlerIndex, TurnCommand } from "./battle"; 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 => { this.scene.getField(true).filter(p => p.summonData).map(p => {
const bypassSpeed = new Utils.BooleanHolder(false); const bypassSpeed = new Utils.BooleanHolder(false);
applyAbAttrs(BypassSpeedChanceAbAttr, p, null, bypassSpeed);
this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
}); });

View File

@ -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);
});