mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-21 06:49:35 +02:00
Fix behavior of Future Sight and Doom Desire
Add test for Future Sight Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
75a114a89f
commit
8167baff14
@ -760,13 +760,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),
|
||||
* and deals damage after the turn count is reached.
|
||||
*/
|
||||
class DelayedAttackTag extends ArenaTag {
|
||||
export class DelayedAttackTag extends ArenaTag {
|
||||
public targetIndex: BattlerIndex;
|
||||
|
||||
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) {
|
||||
super(tagType, 3, sourceMove, sourceId);
|
||||
constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH) {
|
||||
super(tagType, 3, sourceMove, sourceId, side);
|
||||
|
||||
this.targetIndex = targetIndex;
|
||||
this.side = side;
|
||||
}
|
||||
|
||||
lapse(arena: Arena): boolean {
|
||||
@ -1209,7 +1210,7 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove
|
||||
return new ToxicSpikesTag(sourceId, side);
|
||||
case ArenaTagType.FUTURE_SIGHT:
|
||||
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:
|
||||
return new WishTag(turnCount, sourceId, side);
|
||||
case ArenaTagType.STEALTH_ROCK:
|
||||
|
@ -2683,6 +2683,14 @@ export class ElectroShotChargeAttr extends ChargeAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public tagType: ArenaTagType;
|
||||
public chargeAnim: ChargeAnim;
|
||||
@ -2697,13 +2705,18 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||
}
|
||||
|
||||
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 new Promise(resolve => resolve(true));
|
||||
}
|
||||
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
return new Promise(resolve => {
|
||||
if (args.length < 2 || !args[1]) {
|
||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
|
||||
(args[0] as Utils.BooleanHolder).value = true;
|
||||
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.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);
|
||||
});
|
||||
@ -5331,7 +5344,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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -7923,7 +7937,8 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||
.ballBombMove(),
|
||||
new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
|
||||
.partial() // Complete buggy mess
|
||||
.partial()
|
||||
.ignoresProtect()
|
||||
.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)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
||||
@ -8228,7 +8243,8 @@ export function initMoves() {
|
||||
.attr(ConfuseAttr)
|
||||
.pulseMove(),
|
||||
new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
|
||||
.partial() // Complete buggy mess
|
||||
.partial()
|
||||
.ignoresProtect()
|
||||
.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)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
|
||||
|
@ -4,16 +4,16 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
|
||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
import { MoveAnim } from "#app/data/battle-anims";
|
||||
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
|
||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
|
||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, DelayedAttackAttr, OneHitKOAttr, ToxicAccuracyAttr } from "#app/data/move";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier";
|
||||
import i18next from "i18next";
|
||||
import * as Utils from "#app/utils";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
import { Type } from "#app/data/type";
|
||||
|
||||
export class MoveEffectPhase extends PokemonPhase {
|
||||
@ -24,10 +24,10 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
super(scene, battlerIndex);
|
||||
this.move = move;
|
||||
/**
|
||||
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
|
||||
* with no party members available to switch in, then the right Pokemon takes the index
|
||||
* of the left Pokemon and gets hit unless this is checked.
|
||||
*/
|
||||
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
|
||||
* with no party members available to switch in, then the right Pokemon takes the index
|
||||
* of the left Pokemon and gets hit unless this is checked.
|
||||
*/
|
||||
if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
|
||||
const i = targets.indexOf(battlerIndex);
|
||||
targets.splice(i, i + 1);
|
||||
@ -43,15 +43,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
/** All Pokemon targeted by this phase's invoked move */
|
||||
const targets = this.getTargets();
|
||||
|
||||
/** If the user was somehow removed from the field, end this phase */
|
||||
if (!user?.isOnField()) {
|
||||
if (!user) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does an effect from this move override other effects on this turn?
|
||||
* e.g. Charging moves (Fly, etc.) on their first turn of use.
|
||||
*/
|
||||
* Does an effect from this move override other effects on this turn?
|
||||
* e.g. Charging moves (Fly, etc.) on their first turn of use.
|
||||
*/
|
||||
const overridden = new Utils.BooleanHolder(false);
|
||||
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
|
||||
const move = this.move.getMove();
|
||||
@ -66,10 +71,10 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
|
||||
/**
|
||||
* If this phase is for the first hit of the invoked move,
|
||||
* resolve the move's total hit count. This block combines the
|
||||
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
|
||||
*/
|
||||
* If this phase is for the first hit of the invoked move,
|
||||
* resolve the move's total hit count. This block combines the
|
||||
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
|
||||
*/
|
||||
if (user.turnData.hitsLeft === -1) {
|
||||
const hitCount = new Utils.IntegerHolder(1);
|
||||
// Assume single target for multi hit
|
||||
@ -86,16 +91,16 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||
|
||||
/**
|
||||
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
|
||||
* @see {@linkcode hitCheck}
|
||||
*/
|
||||
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
|
||||
* @see {@linkcode hitCheck}
|
||||
*/
|
||||
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
|
||||
const hasActiveTargets = targets.some(t => t.isActive(true));
|
||||
|
||||
@ -104,17 +109,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
&& !targets[0].getTag(SemiInvulnerableTag);
|
||||
|
||||
/**
|
||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
||||
* (and not random target) and failed the hit check against its target (MISS), log the move
|
||||
* as FAILed or MISSed (depending on the conditions above) and end this phase.
|
||||
*/
|
||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
||||
* (and not random target) and failed the hit check against its target (MISS), log the move
|
||||
* as FAILed or MISSed (depending on the conditions above) and end this phase.
|
||||
*/
|
||||
|
||||
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
|
||||
this.stopMultiHit();
|
||||
if (hasActiveTargets) {
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" }));
|
||||
moveHistoryEntry.result = MoveResult.MISS;
|
||||
applyMoveAttrs(MissEffectAttr, user, null, move);
|
||||
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
|
||||
} else {
|
||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
@ -154,9 +159,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
&& !target.getTag(SemiInvulnerableTag);
|
||||
|
||||
/**
|
||||
* If the move missed a target, stop all future hits against that target
|
||||
* and move on to the next target (if there is one).
|
||||
*/
|
||||
* If the move missed a target, stop all future hits against that target
|
||||
* and move on to the next target (if there is one).
|
||||
*/
|
||||
if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) {
|
||||
this.stopMultiHit(target);
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
||||
@ -177,23 +182,23 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Since all fail/miss checks have applied, the move is considered successfully applied.
|
||||
* It's worth noting that if the move has no effect or is protected against, this assignment
|
||||
* is overwritten and the move is logged as a FAIL.
|
||||
*/
|
||||
* Since all fail/miss checks have applied, the move is considered successfully applied.
|
||||
* It's worth noting that if the move has no effect or is protected against, this assignment
|
||||
* is overwritten and the move is logged as a FAIL.
|
||||
*/
|
||||
moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||
|
||||
/**
|
||||
* Stores the result of applying the invoked move to the target.
|
||||
* If the target is protected, the result is always `NO_EFFECT`.
|
||||
* Otherwise, the hit result is based on type effectiveness, immunities,
|
||||
* and other factors that may negate the attack or status application.
|
||||
*
|
||||
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated
|
||||
* (for attack moves) and the target's HP is updated. However, this isn't
|
||||
* made visible to the user until the resulting {@linkcode DamagePhase}
|
||||
* is invoked.
|
||||
*/
|
||||
* Stores the result of applying the invoked move to the target.
|
||||
* If the target is protected, the result is always `NO_EFFECT`.
|
||||
* Otherwise, the hit result is based on type effectiveness, immunities,
|
||||
* and other factors that may negate the attack or status application.
|
||||
*
|
||||
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated
|
||||
* (for attack moves) and the target's HP is updated. However, this isn't
|
||||
* made visible to the user until the resulting {@linkcode DamagePhase}
|
||||
* is invoked.
|
||||
*/
|
||||
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
|
||||
|
||||
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
||||
@ -211,9 +216,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* If the move has no effect on the target (i.e. the target is protected or immune),
|
||||
* change the logged move result to FAIL.
|
||||
*/
|
||||
* If the move has no effect on the target (i.e. the target is protected or immune),
|
||||
* change the logged move result to FAIL.
|
||||
*/
|
||||
if (hitResult === HitResult.NO_EFFECT) {
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
@ -222,19 +227,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
|
||||
|
||||
/**
|
||||
* If the user can change forms by using the invoked move,
|
||||
* it only changes forms after the move's last hit
|
||||
* (see Relic Song's interaction with Parental Bond when used by Meloetta).
|
||||
*/
|
||||
* If the user can change forms by using the invoked move,
|
||||
* it only changes forms after the move's last hit
|
||||
* (see Relic Song's interaction with Parental Bond when used by Meloetta).
|
||||
*/
|
||||
if (lastHit) {
|
||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs.
|
||||
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
|
||||
* type requires different conditions to be met with respect to the move's hit result.
|
||||
*/
|
||||
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs.
|
||||
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
|
||||
* type requires different conditions to be met with respect to the move's hit result.
|
||||
*/
|
||||
applyAttrs.push(new Promise(resolve => {
|
||||
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move)
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT,
|
||||
@ -244,10 +249,10 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
/** Are the move's effects tied to the first turn of a charge move? */
|
||||
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move));
|
||||
/**
|
||||
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
|
||||
* ignore all effects after this point.
|
||||
* Otherwise, apply all self-targeted POST_APPLY effects.
|
||||
*/
|
||||
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
|
||||
* ignore all effects after this point.
|
||||
* Otherwise, apply all self-targeted POST_APPLY effects.
|
||||
*/
|
||||
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => {
|
||||
// All effects past this point require the move to have hit the target
|
||||
@ -256,9 +261,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
|
||||
/**
|
||||
* If the move hit, and the target doesn't have Shield Dust,
|
||||
* apply the chance to flinch the target gained from King's Rock
|
||||
*/
|
||||
* If the move hit, and the target doesn't have Shield Dust,
|
||||
* apply the chance to flinch the target gained from King's Rock
|
||||
*/
|
||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
|
||||
const flinched = new Utils.BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
@ -288,9 +293,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
// Apply the user's post-attack ability effects
|
||||
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
|
||||
/**
|
||||
* If the invoked move is an attack, apply the user's chance to
|
||||
* steal an item from the target granted by Grip Claw
|
||||
*/
|
||||
* If the invoked move is an attack, apply the user's chance to
|
||||
* steal an item from the target granted by Grip Claw
|
||||
*/
|
||||
if (this.move.getMove() instanceof AttackMove) {
|
||||
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||
}
|
||||
@ -345,12 +350,12 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
end() {
|
||||
const user = this.getUserPokemon();
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike,
|
||||
* unshift another MoveEffectPhase for the next strike.
|
||||
* Otherwise, queue a message indicating the number of times the move has struck
|
||||
* (if the move has struck more than once), then apply the heal from Shell Bell
|
||||
* to the user.
|
||||
*/
|
||||
* If this phase isn't for the invoked move's last strike,
|
||||
* unshift another MoveEffectPhase for the next strike.
|
||||
* Otherwise, queue a message indicating the number of times the move has struck
|
||||
* (if the move has struck more than once), then apply the heal from Shell Bell
|
||||
* to the user.
|
||||
*/
|
||||
if (user) {
|
||||
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
|
||||
this.scene.unshiftPhase(this.getNewHitPhase());
|
||||
@ -372,10 +377,10 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether this phase's invoked move hits or misses the given target
|
||||
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move
|
||||
* @returns `true` if the move does not miss the target; `false` otherwise
|
||||
*/
|
||||
* Resolves whether this phase's invoked move hits or misses the given target
|
||||
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move
|
||||
* @returns `true` if the move does not miss the target; `false` otherwise
|
||||
*/
|
||||
hitCheck(target: Pokemon): boolean {
|
||||
// Moves targeting the user and entry hazards can't miss
|
||||
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
|
||||
@ -449,9 +454,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given {@linkcode Pokemon} from this phase's target list
|
||||
* @param target {@linkcode Pokemon} the Pokemon to be removed
|
||||
*/
|
||||
* Removes the given {@linkcode Pokemon} from this phase's target list
|
||||
* @param target {@linkcode Pokemon} the Pokemon to be removed
|
||||
*/
|
||||
removeTarget(target: Pokemon): void {
|
||||
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
|
||||
if (targetIndex !== -1) {
|
||||
@ -460,19 +465,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents subsequent strikes of this phase's invoked move from occurring
|
||||
* @param target {@linkcode Pokemon} if defined, only stop subsequent
|
||||
* strikes against this Pokemon
|
||||
*/
|
||||
* Prevents subsequent strikes of this phase's invoked move from occurring
|
||||
* @param target {@linkcode Pokemon} if defined, only stop subsequent
|
||||
* strikes against this Pokemon
|
||||
*/
|
||||
stopMultiHit(target?: Pokemon): void {
|
||||
/** If given a specific target, remove the target from subsequent strikes */
|
||||
if (target) {
|
||||
this.removeTarget(target);
|
||||
}
|
||||
/**
|
||||
* If no target specified, or the specified target was the last of this move's
|
||||
* targets, completely cancel all subsequent strikes.
|
||||
*/
|
||||
* If no target specified, or the specified target was the last of this move's
|
||||
* targets, completely cancel all subsequent strikes.
|
||||
*/
|
||||
if (!target || this.targets.length === 0 ) {
|
||||
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here?
|
||||
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here?
|
||||
|
@ -1,9 +1,29 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr } from "#app/data/ability";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
applyPostMoveUsedAbAttrs,
|
||||
applyPreAttackAbAttrs,
|
||||
BlockRedirectAbAttr,
|
||||
IncreasePpAbAttr,
|
||||
PokemonTypeChangeAbAttr,
|
||||
PostMoveUsedAbAttr,
|
||||
RedirectMoveAbAttr
|
||||
} from "#app/data/ability";
|
||||
import { CommonAnim } from "#app/data/battle-anims";
|
||||
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
|
||||
import {
|
||||
allMoves,
|
||||
applyMoveAttrs,
|
||||
BypassRedirectAttr,
|
||||
BypassSleepAttr,
|
||||
ChargeAttr,
|
||||
CopyMoveAttr,
|
||||
DelayedAttackAttr,
|
||||
HealStatusEffectAttr,
|
||||
MoveFlags,
|
||||
PreMoveMessageAttr
|
||||
} from "#app/data/move";
|
||||
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
||||
import { Type } from "#app/data/type";
|
||||
@ -22,6 +42,8 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { DelayedAttackTag } from "#app/data/arena-tag";
|
||||
|
||||
export class MovePhase extends BattlePhase {
|
||||
protected _pokemon: Pokemon;
|
||||
@ -217,6 +239,32 @@ export class MovePhase extends BattlePhase {
|
||||
// form changes happen even before we know that the move wll execute.
|
||||
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();
|
||||
|
||||
// TODO: Clean up implementation of two-turn moves.
|
||||
|
45
src/test/moves/future_sight.test.ts
Normal file
45
src/test/moves/future_sight.test.ts
Normal file
@ -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
Block a user