[Move] Improve Future Sight & Doom Desire (still partial) (#4545)
* Fix behavior of Future Sight and Doom Desire Add test for Future Sight Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Prevent crash if Future Sight target is missing Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> * Add `partial()` comments, update `DelayedAttackAttr` return --------- Co-authored-by: Gianluca Fuoco <gianluca_1227@hotmail.com> Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com>
This commit is contained in:
parent
433d4b4fc9
commit
ab6d15ee8a
|
@ -780,13 +780,14 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||||
* Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used),
|
* Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used),
|
||||||
* and deals damage after the turn count is reached.
|
* and deals damage after the turn count is reached.
|
||||||
*/
|
*/
|
||||||
class DelayedAttackTag extends ArenaTag {
|
export class DelayedAttackTag extends ArenaTag {
|
||||||
public targetIndex: BattlerIndex;
|
public targetIndex: BattlerIndex;
|
||||||
|
|
||||||
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) {
|
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH) {
|
||||||
super(tagType, 3, sourceMove, sourceId);
|
super(tagType, 3, sourceMove, sourceId, side);
|
||||||
|
|
||||||
this.targetIndex = targetIndex;
|
this.targetIndex = targetIndex;
|
||||||
|
this.side = side;
|
||||||
}
|
}
|
||||||
|
|
||||||
lapse(arena: Arena): boolean {
|
lapse(arena: Arena): boolean {
|
||||||
|
@ -1250,7 +1251,7 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
|
||||||
return new ToxicSpikesTag(sourceId, side);
|
return new ToxicSpikesTag(sourceId, side);
|
||||||
case ArenaTagType.FUTURE_SIGHT:
|
case ArenaTagType.FUTURE_SIGHT:
|
||||||
case ArenaTagType.DOOM_DESIRE:
|
case ArenaTagType.DOOM_DESIRE:
|
||||||
return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!); // TODO:questionable bang
|
return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!, side); // TODO:questionable bang
|
||||||
case ArenaTagType.WISH:
|
case ArenaTagType.WISH:
|
||||||
return new WishTag(turnCount, sourceId, side);
|
return new WishTag(turnCount, sourceId, side);
|
||||||
case ArenaTagType.STEALTH_ROCK:
|
case ArenaTagType.STEALTH_ROCK:
|
||||||
|
|
|
@ -2785,6 +2785,14 @@ export class OverrideMoveEffectAttr extends MoveAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attack Move that doesn't hit the turn it is played and doesn't allow for multiple
|
||||||
|
* uses on the same target. Examples are Future Sight or Doom Desire.
|
||||||
|
* @extends OverrideMoveEffectAttr
|
||||||
|
* @param tagType The {@linkcode ArenaTagType} that will be placed on the field when the move is used
|
||||||
|
* @param chargeAnim The {@linkcode ChargeAnim | Charging Animation} used for the move
|
||||||
|
* @param chargeText The text to display when the move is used
|
||||||
|
*/
|
||||||
export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||||
public tagType: ArenaTagType;
|
public tagType: ArenaTagType;
|
||||||
public chargeAnim: ChargeAnim;
|
public chargeAnim: ChargeAnim;
|
||||||
|
@ -2799,13 +2807,18 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||||
|
// Edge case for the move applied on a pokemon that has fainted
|
||||||
|
if (!target) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (args.length < 2 || !args[1]) {
|
if (args.length < 2 || !args[1]) {
|
||||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
|
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
|
||||||
(args[0] as Utils.BooleanHolder).value = true;
|
(args[0] as Utils.BooleanHolder).value = true;
|
||||||
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
||||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
||||||
user.scene.arena.addTag(this.tagType, 3, move.id, user.id, ArenaTagSide.BOTH, false, target.getBattlerIndex());
|
user.scene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
|
@ -5534,7 +5547,8 @@ export class AddArenaTagAttr extends MoveEffectAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
||||||
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY);
|
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, side);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8297,7 +8311,8 @@ export function initMoves() {
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||||
.ballBombMove(),
|
.ballBombMove(),
|
||||||
new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
|
new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
|
||||||
.partial() // Complete buggy mess
|
.partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc
|
||||||
|
.ignoresProtect()
|
||||||
.attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })),
|
.attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })),
|
||||||
new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
|
new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
||||||
|
@ -8604,7 +8619,8 @@ export function initMoves() {
|
||||||
.attr(ConfuseAttr)
|
.attr(ConfuseAttr)
|
||||||
.pulseMove(),
|
.pulseMove(),
|
||||||
new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
|
new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
|
||||||
.partial() // Complete buggy mess
|
.partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc
|
||||||
|
.ignoresProtect()
|
||||||
.attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })),
|
.attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })),
|
||||||
new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
|
new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
|
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
applyFilteredMoveAttrs,
|
applyFilteredMoveAttrs,
|
||||||
applyMoveAttrs,
|
applyMoveAttrs,
|
||||||
AttackMove,
|
AttackMove,
|
||||||
|
DelayedAttackAttr,
|
||||||
FixedDamageAttr,
|
FixedDamageAttr,
|
||||||
HitsTagAttr,
|
HitsTagAttr,
|
||||||
MissEffectAttr,
|
MissEffectAttr,
|
||||||
|
@ -85,8 +86,13 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||||
/** All Pokemon targeted by this phase's invoked move */
|
/** All Pokemon targeted by this phase's invoked move */
|
||||||
const targets = this.getTargets();
|
const targets = this.getTargets();
|
||||||
|
|
||||||
/** If the user was somehow removed from the field, end this phase */
|
if (!user) {
|
||||||
if (!user?.isOnField()) {
|
return super.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
|
||||||
|
/** If the user was somehow removed from the field and it's not a delayed attack, end this phase */
|
||||||
|
if (!user.isOnField() && !isDelayedAttack) {
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,9 +148,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||||
const hasActiveTargets = targets.some(t => t.isActive(true));
|
const hasActiveTargets = targets.some(t => t.isActive(true));
|
||||||
|
|
||||||
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
|
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
|
||||||
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr)
|
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
|
||||||
&& (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||||
&& !targets[0].getTag(SemiInvulnerableTag);
|
&& !targets[0]?.getTag(SemiInvulnerableTag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
||||||
|
@ -156,7 +162,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||||
if (hasActiveTargets) {
|
if (hasActiveTargets) {
|
||||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||||
moveHistoryEntry.result = MoveResult.MISS;
|
moveHistoryEntry.result = MoveResult.MISS;
|
||||||
applyMoveAttrs(MissEffectAttr, user, null, move);
|
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
|
||||||
} else {
|
} else {
|
||||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||||
moveHistoryEntry.result = MoveResult.FAIL;
|
moveHistoryEntry.result = MoveResult.FAIL;
|
||||||
|
|
|
@ -1,9 +1,31 @@
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability";
|
import {
|
||||||
|
applyAbAttrs,
|
||||||
|
applyPostMoveUsedAbAttrs,
|
||||||
|
applyPreAttackAbAttrs,
|
||||||
|
BlockRedirectAbAttr,
|
||||||
|
IncreasePpAbAttr,
|
||||||
|
PokemonTypeChangeAbAttr,
|
||||||
|
PostMoveUsedAbAttr,
|
||||||
|
RedirectMoveAbAttr,
|
||||||
|
ReduceStatusEffectDurationAbAttr
|
||||||
|
} from "#app/data/ability";
|
||||||
|
import { DelayedAttackTag } from "#app/data/arena-tag";
|
||||||
import { CommonAnim } from "#app/data/battle-anims";
|
import { CommonAnim } from "#app/data/battle-anims";
|
||||||
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
|
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||||
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, frenzyMissFunc, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
|
import {
|
||||||
|
allMoves,
|
||||||
|
applyMoveAttrs,
|
||||||
|
BypassRedirectAttr,
|
||||||
|
BypassSleepAttr,
|
||||||
|
CopyMoveAttr,
|
||||||
|
DelayedAttackAttr,
|
||||||
|
frenzyMissFunc,
|
||||||
|
HealStatusEffectAttr,
|
||||||
|
MoveFlags,
|
||||||
|
PreMoveMessageAttr
|
||||||
|
} from "#app/data/move";
|
||||||
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
|
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
|
||||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
||||||
import { Type } from "#app/data/type";
|
import { Type } from "#app/data/type";
|
||||||
|
@ -14,16 +36,17 @@ import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import { BattlePhase } from "#app/phases/battle-phase";
|
import { BattlePhase } from "#app/phases/battle-phase";
|
||||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||||
|
import { MoveChargePhase } from "#app/phases/move-charge-phase";
|
||||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||||
import { BooleanHolder, NumberHolder } from "#app/utils";
|
import { BooleanHolder, NumberHolder } from "#app/utils";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { MoveChargePhase } from "#app/phases/move-charge-phase";
|
|
||||||
|
|
||||||
export class MovePhase extends BattlePhase {
|
export class MovePhase extends BattlePhase {
|
||||||
protected _pokemon: Pokemon;
|
protected _pokemon: Pokemon;
|
||||||
|
@ -227,6 +250,32 @@ export class MovePhase extends BattlePhase {
|
||||||
// form changes happen even before we know that the move wll execute.
|
// form changes happen even before we know that the move wll execute.
|
||||||
this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||||
|
|
||||||
|
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
|
||||||
|
if (isDelayedAttack) {
|
||||||
|
// Check the player side arena if future sight is active
|
||||||
|
const futureSightTags = this.scene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT);
|
||||||
|
const doomDesireTags = this.scene.arena.findTags(t => t.tagType === ArenaTagType.DOOM_DESIRE);
|
||||||
|
let fail = false;
|
||||||
|
const currentTargetIndex = targets[0].getBattlerIndex();
|
||||||
|
for (const tag of futureSightTags) {
|
||||||
|
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
|
||||||
|
fail = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const tag of doomDesireTags) {
|
||||||
|
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
|
||||||
|
fail = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fail) {
|
||||||
|
this.showMoveText();
|
||||||
|
this.showFailedText();
|
||||||
|
return this.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.showMoveText();
|
this.showMoveText();
|
||||||
|
|
||||||
if (moveQueue.length > 0) {
|
if (moveQueue.length > 0) {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Future Sight", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.startingLevel(50)
|
||||||
|
.moveset([ Moves.FUTURE_SIGHT, Moves.SPLASH ])
|
||||||
|
.battleType("single")
|
||||||
|
.enemySpecies(Species.MAGIKARP)
|
||||||
|
.enemyAbility(Abilities.STURDY)
|
||||||
|
.enemyMoveset(Moves.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hits 2 turns after use, ignores user switch out", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS, Species.MILOTIC ]);
|
||||||
|
|
||||||
|
game.move.select(Moves.FUTURE_SIGHT);
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue