[Refactor] Remove Promises from moves and abilities (#5283)

* Remove Promises from moves and abilities

* Fix `PostSummonPhase`

* Apply suggestions from Kev's review

* More suggestions

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Cleaning up some updated functions

* Remove Promise from `addEnemyModifier`

+ fixes to some extraneous `await`s

* Test fixes

* Fix missing import in revival blessing test

Co-authored-by: innerthunder <brandonerickson98@gmail.com>

* Add back applyPreLeaveFieldAttrs

Attribute was removed due to absence in a cherry-pick

* Make applyPostApplyEffects work

* Fix move-effect-phase.ts applications

Some applyX methods were missed in the cherry pick commit and were still returning functions instead of running the function themselves

* Mock `BattleScene.addPokemonIcon` in tests

* Revival Blessing condition and tests

* Incorporate Despair-Games/poketernity/pull/48

* Break up imports

* Remove enemy modifier chance dead code

* Remove async from applyAbAttrsInternal

Stray async leftover from merge

* Remove docs and comments referencing promises

* Add `user.setTempAbility` to transform phase

---------

Co-authored-by: innerthunder <brandonerickson98@gmail.com>
Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: PigeonBar <56974298+PigeonBar@users.noreply.github.com>
This commit is contained in:
Sirz Benjie 2025-02-21 02:34:39 -06:00 committed by GitHub
parent 97aeceab58
commit e4ce822ce6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 3277 additions and 1608 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,16 @@
import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; import { ChargeAnim, MoveChargeAnim } from "./battle-anims";
import { CommandedTag, EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import {
CommandedTag,
EncoreTag,
GulpMissileTag,
HelpingHandTag,
SemiInvulnerableTag,
ShellTrapTag,
StockpilingTag,
SubstituteTag,
TrappedTag,
TypeBoostTag,
} from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import type { AttackMoveResult, TurnMove } from "../field/pokemon"; import type { AttackMoveResult, TurnMove } from "../field/pokemon";
import type Pokemon from "../field/pokemon"; import type Pokemon from "../field/pokemon";
@ -30,7 +41,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MoveUsedEvent } from "#app/events/battle-scene"; import { MoveUsedEvent } from "#app/events/battle-scene";
import { BATTLE_STATS, type BattleStat, EFFECTIVE_STATS, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat"; import { BATTLE_STATS, type BattleStat, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat";
import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
@ -46,6 +57,10 @@ import { applyChallenges, ChallengeType } from "./challenge";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
import { LoadMoveAnimPhase } from "#app/phases/load-move-anim-phase";
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
import { MoveAnimPhase } from "#app/phases/move-anim-phase";
export enum MoveCategory { export enum MoveCategory {
PHYSICAL, PHYSICAL,
@ -1057,7 +1072,7 @@ export abstract class MoveAttr {
* @param args Set of unique arguments needed by this attribute * @param args Set of unique arguments needed by this attribute
* @returns true if application of the ability succeeds * @returns true if application of the ability succeeds
*/ */
apply(user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): boolean {
return true; return true;
} }
@ -1200,7 +1215,7 @@ export class MoveEffectAttr extends MoveAttr {
} }
/** Applies move effects so long as they are able based on {@linkcode canApply} */ /** Applies move effects so long as they are able based on {@linkcode canApply} */
apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
return this.canApply(user, target, move, args); return this.canApply(user, target, move, args);
} }
@ -1866,7 +1881,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
* @param args - n/a * @param args - n/a
* @returns A boolean indicating whether the effect was successfully applied. * @returns A boolean indicating whether the effect was successfully applied.
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const targetAlly = target.getAlly(); const targetAlly = target.getAlly();
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
@ -2406,32 +2421,27 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
this.chance = chance; this.chance = chance;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise<boolean>(resolve => { if (move.hitsSubstitute(user, target)) {
if (move.hitsSubstitute(user, target)) { return false;
return resolve(false); }
}
const rand = Phaser.Math.RND.realInRange(0, 1);
if (rand >= this.chance) {
return resolve(false);
}
const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable);
if (heldItems.length) {
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
const highestItemTier = heldItems.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter(m => m.type.getOrInferTier(poolType) === highestItemTier);
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
globalScene.tryTransferHeldItemModifier(stolenItem, user, false).then(success => {
if (success) {
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
}
resolve(success);
});
return;
}
resolve(false); const rand = Phaser.Math.RND.realInRange(0, 1);
}); if (rand >= this.chance) {
return false;
}
const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable);
if (heldItems.length) {
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier);
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
if (globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
return true;
}
}
return false;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
@ -2875,9 +2885,7 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr {
} }
export class OverrideMoveEffectAttr extends MoveAttr { export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
//const overridden = args[0] as Utils.BooleanHolder;
//const virtual = arg[1] as boolean;
return true; return true;
} }
} }
@ -2903,26 +2911,27 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
this.chargeText = chargeText; this.chargeText = chargeText;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// Edge case for the move applied on a pokemon that has fainted // Edge case for the move applied on a pokemon that has fainted
if (!target) { if (!target) {
return Promise.resolve(true); return 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(false, () => {
(args[0] as Utils.BooleanHolder).value = true;
globalScene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
resolve(true); const overridden = args[0] as Utils.BooleanHolder;
}); const virtual = args[1] as boolean;
} else {
globalScene.ui.showText(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }), null, () => resolve(true)); if (!virtual) {
} overridden.value = true;
}); globalScene.unshiftPhase(new MoveAnimPhase(new MoveChargeAnim(this.chargeAnim, move.id, user)));
globalScene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
} else {
globalScene.queueMessage(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }));
}
return true;
} }
} }
@ -3053,7 +3062,7 @@ export class StatStageChangeAttr extends MoveEffectAttr {
* @param args unused * @param args unused
* @returns whether stat stages were changed * @returns whether stat stages were changed
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) { if (!super.apply(user, target, move, args) || (this.condition && !this.condition(user, target, move))) {
return false; return false;
} }
@ -3131,7 +3140,7 @@ export class SecretPowerAttr extends MoveEffectAttr {
* Used to apply the secondary effect to the target Pokemon * Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied * @returns `true` if a secondary effect is successfully applied
*/ */
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
return false; return false;
} }
@ -3286,8 +3295,8 @@ export class AcupressureStatStageChangeAttr extends MoveEffectAttr {
super(); super();
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const randStats = BATTLE_STATS.filter(s => target.getStatStage(s) < 6); const randStats = BATTLE_STATS.filter((s) => target.getStatStage(s) < 6);
if (randStats.length > 0) { if (randStats.length > 0) {
const boostStat = [ randStats[user.randSeedInt(randStats.length)] ]; const boostStat = [ randStats[user.randSeedInt(randStats.length)] ];
globalScene.unshiftPhase(new StatStageChangePhase(target.getBattlerIndex(), this.selfTarget, boostStat, 2)); globalScene.unshiftPhase(new StatStageChangePhase(target.getBattlerIndex(), this.selfTarget, boostStat, 2));
@ -3324,17 +3333,14 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
this.messageCallback = messageCallback; this.messageCallback = messageCallback;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise<boolean>(resolve => { user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true);
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true); user.updateInfo();
user.updateInfo().then(() => { const ret = super.apply(user, target, move, args);
const ret = super.apply(user, target, move, args); if (this.messageCallback) {
if (this.messageCallback) { this.messageCallback(user);
this.messageCallback(user); }
} return ret;
resolve(ret);
});
});
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
@ -3426,28 +3432,27 @@ export class ResetStatsAttr extends MoveEffectAttr {
super(); super();
this.targetAllPokemon = targetAllPokemon; this.targetAllPokemon = targetAllPokemon;
} }
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
const promises: Promise<void>[] = []; override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (this.targetAllPokemon) { // Target all pokemon on the field when Freezy Frost or Haze are used if (this.targetAllPokemon) {
// Target all pokemon on the field when Freezy Frost or Haze are used
const activePokemon = globalScene.getField(true); const activePokemon = globalScene.getField(true);
activePokemon.forEach(p => promises.push(this.resetStats(p))); activePokemon.forEach((p) => this.resetStats(p));
globalScene.queueMessage(i18next.t("moveTriggers:statEliminated")); globalScene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used } else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) { if (!move.hitsSubstitute(user, target)) {
promises.push(this.resetStats(target)); this.resetStats(target);
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) })); globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
} }
} }
await Promise.all(promises);
return true; return true;
} }
async resetStats(pokemon: Pokemon): Promise<void> { private resetStats(pokemon: Pokemon): void {
for (const s of BATTLE_STATS) { for (const s of BATTLE_STATS) {
pokemon.setStatStage(s, 0); pokemon.setStatStage(s, 0);
} }
return pokemon.updateInfo(); pokemon.updateInfo();
} }
} }
@ -3503,43 +3508,28 @@ export class SwapStatStagesAttr extends MoveEffectAttr {
} }
export class HpSplitAttr extends MoveEffectAttr { export class HpSplitAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise(resolve => { if (!super.apply(user, target, move, args)) {
if (!super.apply(user, target, move, args)) { return false;
return resolve(false); }
}
const infoUpdates: Promise<void>[] = []; const hpValue = Math.floor((target.hp + user.hp) / 2);
[ user, target ].forEach((p) => {
const hpValue = Math.floor((target.hp + user.hp) / 2); if (p.hp < hpValue) {
if (user.hp < hpValue) { const healing = p.heal(hpValue - p.hp);
const healing = user.heal(hpValue - user.hp);
if (healing) { if (healing) {
globalScene.damageNumberHandler.add(user, healing, HitResult.HEAL); globalScene.damageNumberHandler.add(p, healing, HitResult.HEAL);
} }
} else if (user.hp > hpValue) { } else if (p.hp > hpValue) {
const damage = user.damage(user.hp - hpValue, true); const damage = p.damage(p.hp - hpValue, true);
if (damage) { if (damage) {
globalScene.damageNumberHandler.add(user, damage); globalScene.damageNumberHandler.add(p, damage);
} }
} }
infoUpdates.push(user.updateInfo()); p.updateInfo();
if (target.hp < hpValue) {
const healing = target.heal(hpValue - target.hp);
if (healing) {
globalScene.damageNumberHandler.add(user, healing, HitResult.HEAL);
}
} else if (target.hp > hpValue) {
const damage = target.damage(target.hp - hpValue, true);
if (damage) {
globalScene.damageNumberHandler.add(target, damage);
}
}
infoUpdates.push(target.updateInfo());
return Promise.all(infoUpdates).then(() => resolve(true));
}); });
return true;
} }
} }
@ -6024,44 +6014,44 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
* @param args N/A * @param args N/A
* @returns Promise, true if function succeeds. * @returns Promise, true if function succeeds.
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise(resolve => { // If user is player, checks if the user has fainted pokemon
// If user is player, checks if the user has fainted pokemon if (user instanceof PlayerPokemon) {
if (user instanceof PlayerPokemon globalScene.unshiftPhase(new RevivalBlessingPhase(user));
&& globalScene.getPlayerParty().findIndex(p => p.isFainted()) > -1) { return true;
(user as PlayerPokemon).revivalBlessing().then(() => { } else if (user instanceof EnemyPokemon && user.hasTrainer() && globalScene.getEnemyParty().findIndex((p) => p.isFainted() && !p.isBoss()) > -1) {
resolve(true); // If used by an enemy trainer with at least one fainted non-boss Pokemon, this
}); // revives one of said Pokemon selected at random.
// If user is enemy, checks that it is a trainer, and it has fainted non-boss pokemon in party const faintedPokemon = globalScene.getEnemyParty().filter((p) => p.isFainted() && !p.isBoss());
} else if (user instanceof EnemyPokemon const pokemon = faintedPokemon[user.randSeedInt(faintedPokemon.length)];
&& user.hasTrainer() const slotIndex = globalScene.getEnemyParty().findIndex((p) => pokemon.id === p.id);
&& globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) { pokemon.resetStatus();
// Selects a random fainted pokemon pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
const faintedPokemon = globalScene.getEnemyParty().filter(p => p.isFainted() && !p.isBoss()); globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
const pokemon = faintedPokemon[user.randSeedInt(faintedPokemon.length)];
const slotIndex = globalScene.getEnemyParty().findIndex(p => pokemon.id === p.id);
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) { if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) {
const allyPokemon = user.getAlly(); const allyPokemon = user.getAlly();
if (slotIndex <= 1) { if (slotIndex <= 1) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false)); globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false));
} else if (allyPokemon.isFainted()) { } else if (allyPokemon.isFainted()) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false)); globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false));
}
} }
resolve(true);
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
resolve(false);
} }
}); return true;
}
return false;
} }
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getCondition(): MoveConditionFunc {
if (user.hasTrainer() && globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) { return (user, target, move) =>
(user instanceof PlayerPokemon && globalScene.getPlayerParty().some((p) => p.isFainted())) ||
(user instanceof EnemyPokemon &&
user.hasTrainer() &&
globalScene.getEnemyParty().some((p) => p.isFainted() && !p.isBoss()));
}
override getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
if (user.hasTrainer() && globalScene.getEnemyParty().some((p) => p.isFainted() && !p.isBoss())) {
return 20; return 20;
} }
@ -6579,7 +6569,7 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
class CallMoveAttr extends OverrideMoveEffectAttr { class CallMoveAttr extends OverrideMoveEffectAttr {
protected invalidMoves: Moves[]; protected invalidMoves: Moves[];
protected hasTarget: boolean; protected hasTarget: boolean;
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined; const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget); const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
if (moveTargets.targets.length === 0) { if (moveTargets.targets.length === 0) {
@ -6589,11 +6579,8 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
? moveTargets.targets ? moveTargets.targets
: [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already : [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already
user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true });
globalScene.unshiftPhase(new LoadMoveAnimPhase(move.id));
globalScene.unshiftPhase(new MovePhase(user, targets, new PokemonMove(move.id, 0, 0, true), true, true)); globalScene.unshiftPhase(new MovePhase(user, targets, new PokemonMove(move.id, 0, 0, true), true, true));
await Promise.resolve(initMoveAnim(move.id).then(() => {
loadMoveAnimAssets([ move.id ], true);
}));
return true; return true;
} }
} }
@ -6626,7 +6613,7 @@ export class RandomMoveAttr extends CallMoveAttr {
* @param move Move being used * @param move Move being used
* @param args Unused * @param args Unused
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const moveIds = Utils.getEnumValues(Moves).map(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)") ? m : Moves.NONE); const moveIds = Utils.getEnumValues(Moves).map(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)") ? m : Moves.NONE);
let moveId: Moves = Moves.NONE; let moveId: Moves = Moves.NONE;
do { do {
@ -6663,7 +6650,7 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
* @param move Move being used * @param move Move being used
* @param args Unused * @param args Unused
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return super.apply(user, target, allMoves[this.moveId], args); return super.apply(user, target, allMoves[this.moveId], args);
} }
@ -6965,145 +6952,141 @@ const invalidCopycatMoves = [
]; ];
export class NaturePowerAttr extends OverrideMoveEffectAttr { export class NaturePowerAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return new Promise(resolve => { let moveId;
let moveId; switch (globalScene.arena.getTerrainType()) {
switch (globalScene.arena.getTerrainType()) { // this allows terrains to 'override' the biome move
// this allows terrains to 'override' the biome move case TerrainType.NONE:
case TerrainType.NONE: switch (globalScene.arena.biomeType) {
switch (globalScene.arena.biomeType) { case Biome.TOWN:
case Biome.TOWN: moveId = Moves.ROUND;
moveId = Moves.ROUND; break;
break; case Biome.METROPOLIS:
case Biome.METROPOLIS: moveId = Moves.TRI_ATTACK;
moveId = Moves.TRI_ATTACK; break;
break; case Biome.SLUM:
case Biome.SLUM: moveId = Moves.SLUDGE_BOMB;
moveId = Moves.SLUDGE_BOMB; break;
break; case Biome.PLAINS:
case Biome.PLAINS: moveId = Moves.SILVER_WIND;
moveId = Moves.SILVER_WIND; break;
break; case Biome.GRASS:
case Biome.GRASS: moveId = Moves.GRASS_KNOT;
moveId = Moves.GRASS_KNOT; break;
break; case Biome.TALL_GRASS:
case Biome.TALL_GRASS: moveId = Moves.POLLEN_PUFF;
moveId = Moves.POLLEN_PUFF; break;
break; case Biome.MEADOW:
case Biome.MEADOW: moveId = Moves.GIGA_DRAIN;
moveId = Moves.GIGA_DRAIN; break;
break; case Biome.FOREST:
case Biome.FOREST: moveId = Moves.BUG_BUZZ;
moveId = Moves.BUG_BUZZ; break;
break; case Biome.JUNGLE:
case Biome.JUNGLE: moveId = Moves.LEAF_STORM;
moveId = Moves.LEAF_STORM; break;
break; case Biome.SEA:
case Biome.SEA: moveId = Moves.HYDRO_PUMP;
moveId = Moves.HYDRO_PUMP; break;
break; case Biome.SWAMP:
case Biome.SWAMP: moveId = Moves.MUD_BOMB;
moveId = Moves.MUD_BOMB; break;
break; case Biome.BEACH:
case Biome.BEACH: moveId = Moves.SCALD;
moveId = Moves.SCALD; break;
break; case Biome.LAKE:
case Biome.LAKE: moveId = Moves.BUBBLE_BEAM;
moveId = Moves.BUBBLE_BEAM; break;
break; case Biome.SEABED:
case Biome.SEABED: moveId = Moves.BRINE;
moveId = Moves.BRINE; break;
break; case Biome.ISLAND:
case Biome.ISLAND: moveId = Moves.LEAF_TORNADO;
moveId = Moves.LEAF_TORNADO; break;
break; case Biome.MOUNTAIN:
case Biome.MOUNTAIN: moveId = Moves.AIR_SLASH;
moveId = Moves.AIR_SLASH; break;
break; case Biome.BADLANDS:
case Biome.BADLANDS: moveId = Moves.EARTH_POWER;
moveId = Moves.EARTH_POWER; break;
break; case Biome.DESERT:
case Biome.DESERT: moveId = Moves.SCORCHING_SANDS;
moveId = Moves.SCORCHING_SANDS; break;
break; case Biome.WASTELAND:
case Biome.WASTELAND: moveId = Moves.DRAGON_PULSE;
moveId = Moves.DRAGON_PULSE; break;
break; case Biome.CONSTRUCTION_SITE:
case Biome.CONSTRUCTION_SITE: moveId = Moves.STEEL_BEAM;
moveId = Moves.STEEL_BEAM; break;
break; case Biome.CAVE:
case Biome.CAVE: moveId = Moves.POWER_GEM;
moveId = Moves.POWER_GEM; break;
break; case Biome.ICE_CAVE:
case Biome.ICE_CAVE: moveId = Moves.ICE_BEAM;
moveId = Moves.ICE_BEAM; break;
break; case Biome.SNOWY_FOREST:
case Biome.SNOWY_FOREST: moveId = Moves.FROST_BREATH;
moveId = Moves.FROST_BREATH; break;
break; case Biome.VOLCANO:
case Biome.VOLCANO: moveId = Moves.LAVA_PLUME;
moveId = Moves.LAVA_PLUME; break;
break; case Biome.GRAVEYARD:
case Biome.GRAVEYARD: moveId = Moves.SHADOW_BALL;
moveId = Moves.SHADOW_BALL; break;
break; case Biome.RUINS:
case Biome.RUINS: moveId = Moves.ANCIENT_POWER;
moveId = Moves.ANCIENT_POWER; break;
break; case Biome.TEMPLE:
case Biome.TEMPLE: moveId = Moves.EXTRASENSORY;
moveId = Moves.EXTRASENSORY; break;
break; case Biome.DOJO:
case Biome.DOJO: moveId = Moves.FOCUS_BLAST;
moveId = Moves.FOCUS_BLAST; break;
break; case Biome.FAIRY_CAVE:
case Biome.FAIRY_CAVE: moveId = Moves.ALLURING_VOICE;
moveId = Moves.ALLURING_VOICE; break;
break; case Biome.ABYSS:
case Biome.ABYSS: moveId = Moves.OMINOUS_WIND;
moveId = Moves.OMINOUS_WIND; break;
break; case Biome.SPACE:
case Biome.SPACE: moveId = Moves.DRACO_METEOR;
moveId = Moves.DRACO_METEOR; break;
break; case Biome.FACTORY:
case Biome.FACTORY: moveId = Moves.FLASH_CANNON;
moveId = Moves.FLASH_CANNON; break;
break; case Biome.LABORATORY:
case Biome.LABORATORY: moveId = Moves.ZAP_CANNON;
moveId = Moves.ZAP_CANNON; break;
break; case Biome.POWER_PLANT:
case Biome.POWER_PLANT: moveId = Moves.CHARGE_BEAM;
moveId = Moves.CHARGE_BEAM; break;
break; case Biome.END:
case Biome.END: moveId = Moves.ETERNABEAM;
moveId = Moves.ETERNABEAM; break;
break; }
} break;
break; case TerrainType.MISTY:
case TerrainType.MISTY: moveId = Moves.MOONBLAST;
moveId = Moves.MOONBLAST; break;
break; case TerrainType.ELECTRIC:
case TerrainType.ELECTRIC: moveId = Moves.THUNDERBOLT;
moveId = Moves.THUNDERBOLT; break;
break; case TerrainType.GRASSY:
case TerrainType.GRASSY: moveId = Moves.ENERGY_BALL;
moveId = Moves.ENERGY_BALL; break;
break; case TerrainType.PSYCHIC:
case TerrainType.PSYCHIC: moveId = Moves.PSYCHIC;
moveId = Moves.PSYCHIC; break;
break; default:
default: // Just in case there's no match
// Just in case there's no match moveId = Moves.TRI_ATTACK;
moveId = Moves.TRI_ATTACK; break;
break; }
}
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true }); user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true)); globalScene.unshiftPhase(new LoadMoveAnimPhase(moveId));
initMoveAnim(moveId).then(() => { globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true));
loadMoveAnimAssets([ moveId ], true) return true;
.then(() => resolve(true));
});
});
} }
} }
@ -7121,7 +7104,7 @@ export class CopyMoveAttr extends CallMoveAttr {
this.invalidMoves = invalidMoves; this.invalidMoves = invalidMoves;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
this.hasTarget = this.mirrorMove; this.hasTarget = this.mirrorMove;
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove; const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove;
return super.apply(user, target, allMoves[lastMove], args); return super.apply(user, target, allMoves[lastMove], args);
@ -7682,50 +7665,15 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
* Used by Transform * Used by Transform
*/ */
export class TransformAttr extends MoveEffectAttr { export class TransformAttr extends MoveEffectAttr {
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) { if (!super.apply(user, target, move, args)) {
return false; return false;
} }
const promises: Promise<void>[] = []; globalScene.unshiftPhase(new PokemonTransformPhase(user.getBattlerIndex(), target.getBattlerIndex()));
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.gender = target.getGender();
// Power Trick's effect will not preserved after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false);
}
// Copy all stat stages
for (const s of BATTLE_STATS) {
user.setStatStage(s, target.getStatStage(s));
}
user.summonData.moveset = target.getMoveset().map((m) => {
if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
} else {
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(Moves.NONE);
}
});
user.summonData.types = target.getTypes();
promises.push(user.updateInfo());
globalScene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); globalScene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
promises.push(user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
// If the new ability activates immediately, it needs to happen after all the transform animations
user.setTempAbility(target.getAbility());
}));
await Promise.all(promises);
return true; return true;
} }
} }
@ -8128,44 +8076,54 @@ const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) =
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(
return new Promise(resolve => { attrFilter: MoveAttrFilter,
const attrPromises: Promise<boolean>[] = []; user: Pokemon | null,
const moveAttrs = move.attrs.filter(a => attrFilter(a)); target: Pokemon | null,
for (const attr of moveAttrs) { move: Move,
const result = attr.apply(user, target, move, args); args: any[],
if (result instanceof Promise) { ): void {
attrPromises.push(result); move.attrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args));
}
}
Promise.allSettled(attrPromises).then(() => resolve());
});
} }
function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise<void> { function applyMoveChargeAttrsInternal(
return new Promise(resolve => { attrFilter: MoveAttrFilter,
const chargeAttrPromises: Promise<boolean>[] = []; user: Pokemon | null,
const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a)); target: Pokemon | null,
for (const attr of chargeMoveAttrs) { move: ChargingMove,
const result = attr.apply(user, target, move, args); args: any[],
if (result instanceof Promise) { ): void {
chargeAttrPromises.push(result); move.chargeAttrs.filter((attr) => attrFilter(attr)).forEach((attr) => attr.apply(user, target, move, args));
}
}
Promise.allSettled(chargeAttrPromises).then(() => resolve());
});
} }
export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> { export function applyMoveAttrs(
return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); attrType: Constructor<MoveAttr>,
user: Pokemon | null,
target: Pokemon | null,
move: Move,
...args: any[]
): void {
applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
} }
export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> { export function applyFilteredMoveAttrs(
return applyMoveAttrsInternal(attrFilter, user, target, move, args); attrFilter: MoveAttrFilter,
user: Pokemon,
target: Pokemon | null,
move: Move,
...args: any[]
): void {
applyMoveAttrsInternal(attrFilter, user, target, move, args);
} }
export function applyMoveChargeAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise<void> { export function applyMoveChargeAttrs(
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); attrType: Constructor<MoveAttr>,
user: Pokemon | null,
target: Pokemon | null,
move: ChargingMove,
...args: any[]
): void {
applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
} }
export class MoveCondition { export class MoveCondition {

View File

@ -151,7 +151,7 @@ async function spawnNextTrainerOrEndEncounter() {
// Give 10x Voucher // Give 10x Voucher
const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier();
await globalScene.addModifier(newModifier); globalScene.addModifier(newModifier);
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }));

View File

@ -406,7 +406,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
// Copy old items to new pokemon // Copy old items to new pokemon
for (const item of transformation.heldItems) { for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id; item.pokemonId = newPokemon.id;
await globalScene.addModifier(item, false, false, false, true); globalScene.addModifier(item, false, false, false, true);
} }
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
@ -416,7 +416,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon); const modifier = modType?.newModifier(newPokemon);
if (modifier) { if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
} }
} }

View File

@ -326,7 +326,7 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon); const modifier = modType?.newModifier(pokemon);
if (modifier) { if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
pokemon.calculateStats(); pokemon.calculateStats();
} }
} }
@ -359,7 +359,7 @@ export async function applyModifierTypeToPlayerPokemon(pokemon: PlayerPokemon, m
return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType); return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType);
} }
await globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
} }
/** /**

View File

@ -104,7 +104,6 @@ import { MoveEndPhase } from "#app/phases/move-end-phase";
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase"; import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
@ -4510,43 +4509,6 @@ export class PlayerPokemon extends Pokemon {
this.friendship = Math.max(this.friendship + friendship, 0); this.friendship = Math.max(this.friendship + friendship, 0);
} }
} }
/**
* Handles Revival Blessing when used by player.
* @returns Promise to revive a pokemon.
* @see {@linkcode RevivalBlessingAttr}
*/
revivalBlessing(): Promise<void> {
return new Promise(resolve => {
globalScene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:number, option: PartyOption) => {
if (slotIndex >= 0 && slotIndex < 6) {
const pokemon = globalScene.getPlayerParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {
resolve();
}
pokemon.resetTurnData();
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true);
if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1) {
const allyPokemon = this.getAlly();
if (slotIndex <= 1) {
// Revived ally pokemon
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, true));
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
} else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, true));
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
}
}
}
globalScene.ui.setMode(Mode.MESSAGE).then(() => resolve());
}, PartyUiHandler.FilterFainted);
});
}
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> { getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {
if (!evolution) { if (!evolution) {
@ -4728,70 +4690,62 @@ export class PlayerPokemon extends Pokemon {
} }
/** /**
* Returns a Promise to fuse two PlayerPokemon together * Returns a Promise to fuse two PlayerPokemon together
* @param pokemon The PlayerPokemon to fuse to this one * @param pokemon The PlayerPokemon to fuse to this one
*/ */
fuse(pokemon: PlayerPokemon): Promise<void> { fuse(pokemon: PlayerPokemon): void {
return new Promise(resolve => { this.fusionSpecies = pokemon.species;
this.fusionSpecies = pokemon.species; this.fusionFormIndex = pokemon.formIndex;
this.fusionFormIndex = pokemon.formIndex; this.fusionAbilityIndex = pokemon.abilityIndex;
this.fusionAbilityIndex = pokemon.abilityIndex; this.fusionShiny = pokemon.shiny;
this.fusionShiny = pokemon.shiny; this.fusionVariant = pokemon.variant;
this.fusionVariant = pokemon.variant; this.fusionGender = pokemon.gender;
this.fusionGender = pokemon.gender; this.fusionLuck = pokemon.luck;
this.fusionLuck = pokemon.luck; this.fusionCustomPokemonData = pokemon.customPokemonData;
this.fusionCustomPokemonData = pokemon.customPokemonData; if (pokemon.pauseEvolutions || this.pauseEvolutions) {
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) { this.pauseEvolutions = true;
this.pauseEvolutions = true; }
}
globalScene.validateAchv(achvs.SPLICE); globalScene.validateAchv(achvs.SPLICE);
globalScene.gameData.gameStats.pokemonFused++; globalScene.gameData.gameStats.pokemonFused++;
// Store the average HP% that each Pokemon has // Store the average HP% that each Pokemon has
const maxHp = this.getMaxHp(); const maxHp = this.getMaxHp();
const newHpPercent = ((pokemon.hp / pokemon.getMaxHp()) + (this.hp / maxHp)) / 2; const newHpPercent = (pokemon.hp / pokemon.getMaxHp() + this.hp / maxHp) / 2;
this.generateName(); this.generateName();
this.calculateStats(); this.calculateStats();
// Set this Pokemon's HP to the average % of both fusion components // Set this Pokemon's HP to the average % of both fusion components
this.hp = Math.round(maxHp * newHpPercent); this.hp = Math.round(maxHp * newHpPercent);
if (!this.isFainted()) { if (!this.isFainted()) {
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum // If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
this.hp = Math.min(this.hp, maxHp); this.hp = Math.min(this.hp, maxHp);
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
} else if (!pokemon.isFainted()) { } else if (!pokemon.isFainted()) {
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero // If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
this.hp = Math.max(this.hp, 1); this.hp = Math.max(this.hp, 1);
this.status = pokemon.status; // Inherit the other Pokemon's status this.status = pokemon.status; // Inherit the other Pokemon's status
} }
this.generateCompatibleTms(); this.generateCompatibleTms();
this.updateInfo(true); this.updateInfo(true);
const fusedPartyMemberIndex = globalScene.getPlayerParty().indexOf(pokemon); const fusedPartyMemberIndex = globalScene.getPlayerParty().indexOf(pokemon);
let partyMemberIndex = globalScene.getPlayerParty().indexOf(this); let partyMemberIndex = globalScene.getPlayerParty().indexOf(this);
if (partyMemberIndex > fusedPartyMemberIndex) { if (partyMemberIndex > fusedPartyMemberIndex) {
partyMemberIndex--; partyMemberIndex--;
} }
const fusedPartyMemberHeldModifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier const fusedPartyMemberHeldModifiers = globalScene.findModifiers((m) => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
&& m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; for (const modifier of fusedPartyMemberHeldModifiers) {
const transferModifiers: Promise<boolean>[] = []; globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false);
for (const modifier of fusedPartyMemberHeldModifiers) { }
transferModifiers.push(globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false)); globalScene.updateModifiers(true, true);
} globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
Promise.allSettled(transferModifiers).then(() => { globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
globalScene.updateModifiers(true, true).then(() => { const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex); pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; pokemon.destroy();
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); this.updateFusionPalette();
pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
pokemon.destroy();
this.updateFusionPalette();
resolve();
});
});
});
} }
unfuse(): Promise<void> { unfuse(): Promise<void> {

View File

@ -158,7 +158,7 @@ export abstract class Modifier {
* Handles applying of {@linkcode Modifier} * Handles applying of {@linkcode Modifier}
* @param args collection of all passed parameters * @param args collection of all passed parameters
*/ */
abstract apply(...args: unknown[]): boolean | Promise<boolean>; abstract apply(...args: unknown[]): boolean;
} }
export abstract class PersistentModifier extends Modifier { export abstract class PersistentModifier extends Modifier {
@ -1949,7 +1949,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
* @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item * @param playerPokemon The {@linkcode PlayerPokemon} that consumes the item
* @param args Additional arguments passed to {@linkcode ConsumablePokemonModifier.apply} * @param args Additional arguments passed to {@linkcode ConsumablePokemonModifier.apply}
*/ */
abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise<boolean>; abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean;
getPokemon() { getPokemon() {
return globalScene.getPlayerParty().find(p => p.id === this.pokemonId); return globalScene.getPlayerParty().find(p => p.id === this.pokemonId);
@ -2288,8 +2288,8 @@ export class FusePokemonModifier extends ConsumablePokemonModifier {
* @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon} * @param playerPokemon2 {@linkcode PlayerPokemon} that should be fused with {@linkcode playerPokemon}
* @returns always Promise<true> * @returns always Promise<true>
*/ */
override async apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): Promise<boolean> { override apply(playerPokemon: PlayerPokemon, playerPokemon2: PlayerPokemon): boolean {
await playerPokemon.fuse(playerPokemon2); playerPokemon.fuse(playerPokemon2);
return true; return true;
} }
} }
@ -3136,8 +3136,6 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct?
let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier); let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier);
const heldItemTransferPromises: Promise<void>[] = [];
for (let i = 0; i < transferredItemCount; i++) { for (let i = 0; i < transferredItemCount; i++) {
if (!tierItemModifiers.length) { if (!tierItemModifiers.length) {
while (highestItemTier-- && !tierItemModifiers.length) { while (highestItemTier-- && !tierItemModifiers.length) {
@ -3149,19 +3147,15 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
} }
const randItemIndex = pokemon.randSeedInt(itemModifiers.length); const randItemIndex = pokemon.randSeedInt(itemModifiers.length);
const randItem = itemModifiers[randItemIndex]; const randItem = itemModifiers[randItemIndex];
heldItemTransferPromises.push(globalScene.tryTransferHeldItemModifier(randItem, pokemon, false).then(success => { if (globalScene.tryTransferHeldItemModifier(randItem, pokemon, false)) {
if (success) { transferredModifierTypes.push(randItem.type);
transferredModifierTypes.push(randItem.type); itemModifiers.splice(randItemIndex, 1);
itemModifiers.splice(randItemIndex, 1); }
}
}));
} }
Promise.all(heldItemTransferPromises).then(() => { for (const mt of transferredModifierTypes) {
for (const mt of transferredModifierTypes) { globalScene.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt));
globalScene.queueMessage(this.getTransferMessage(pokemon, targetPokemon, mt)); }
}
});
return !!transferredModifierTypes.length; return !!transferredModifierTypes.length;
} }

View File

@ -21,6 +21,7 @@ export class AddEnemyBuffModifierPhase extends Phase {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
globalScene.addEnemyModifier(getEnemyBuffModifierForWave(tier, globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false)), true, true); globalScene.addEnemyModifier(getEnemyBuffModifierForWave(tier, globalScene.findModifiers(m => m instanceof EnemyPersistentModifier, false)), true, true);
} }
globalScene.updateModifiers(false, true).then(() => this.end()); globalScene.updateModifiers(false, true);
this.end();
} }
} }

View File

@ -63,6 +63,7 @@ export class BattleEndPhase extends BattlePhase {
} }
} }
globalScene.updateModifiers().then(() => this.end()); globalScene.updateModifiers();
this.end();
} }
} }

View File

@ -12,16 +12,22 @@ export class GameOverModifierRewardPhase extends ModifierRewardPhase {
doReward(): Promise<void> { doReward(): Promise<void> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier(); const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => { globalScene.addModifier(newModifier);
// Sound loaded into game as is // Sound loaded into game as is
globalScene.playSound("level_up_fanfare"); globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE); globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.fadeIn(250).then(() => { globalScene.ui.fadeIn(250).then(() => {
globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => { globalScene.ui.showText(
i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }),
null,
() => {
globalScene.time.delayedCall(1500, () => globalScene.arenaBg.setVisible(true)); globalScene.time.delayedCall(1500, () => globalScene.arenaBg.setVisible(true));
resolve(); resolve();
}, null, true, 1500); },
}); null,
true,
1500,
);
}); });
}); });
} }

View File

@ -0,0 +1,20 @@
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import type { Moves } from "#enums/moves";
import { Phase } from "#app/phase";
/**
* Phase for synchronous move animation loading.
* Should be used when a move invokes another move that
* isn't already loaded (e.g. for Metronome)
*/
export class LoadMoveAnimPhase extends Phase {
constructor(protected moveId: Moves) {
super();
}
public override start(): void {
initMoveAnim(this.moveId)
.then(() => loadMoveAnimAssets([ this.moveId ], true))
.then(() => this.end());
}
}

View File

@ -22,10 +22,9 @@ export class ModifierRewardPhase extends BattlePhase {
doReward(): Promise<void> { doReward(): Promise<void> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier(); const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => { globalScene.addModifier(newModifier);
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true); globalScene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true);
});
}); });
} }
} }

View File

@ -0,0 +1,20 @@
import type { MoveAnim } from "#app/data/battle-anims";
import { Phase } from "#app/phase";
/**
* Plays the given {@linkcode MoveAnim} sequentially.
*/
export class MoveAnimPhase<Anim extends MoveAnim> extends Phase {
constructor(
protected anim: Anim,
protected onSubstitute: boolean = false,
) {
super();
}
public override start(): void {
super.start();
this.anim.play(this.onSubstitute, () => this.end());
}
}

View File

@ -44,10 +44,9 @@ export class MoveChargePhase extends PokemonPhase {
new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => { new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => {
move.showChargeText(user, target); move.showChargeText(user, target);
applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => { applyMoveChargeAttrs(MoveEffectAttr, user, target, move);
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
this.end(); this.end();
});
}); });
} }

View File

@ -61,7 +61,7 @@ import {
PokemonMultiHitModifier, PokemonMultiHitModifier,
} from "#app/modifier/modifier"; } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#app/utils";
import { type nil } from "#app/utils"; import { type nil } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { Moves } from "#enums/moves"; import type { Moves } from "#enums/moves";
@ -143,86 +143,86 @@ export class MoveEffectPhase extends PokemonPhase {
const move = this.move.getMove(); const move = this.move.getMove();
// Assume single target for override // Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => { applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual);
// If other effects were overriden, stop this phase before they can be applied
if (overridden.value) { // If other effects were overriden, stop this phase before they can be applied
return this.end(); if (overridden.value) {
return this.end();
}
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that
// the move executes correctly (ensures all hits of a multi-hit are properly calculated)
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) {
user.turnData.hitsLeft = -1;
user.turnData.hitCount = 0;
user.turnData.extraTurns--;
}
/**
* 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 NumberHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, add another hit
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
user.turnData.hitsLeft = hitCount.value;
}
/**
* 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}
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
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 */
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag);
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT));
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, 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 || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit();
if (hasActiveTargets) {
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
} }
user.pushMoveHistory(moveHistoryEntry);
return this.end();
}
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); const playOnEmptyField = globalScene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(
// the move executes correctly (ensures all hits of a multi-hit are properly calculated) move.hitsSubstitute(user, this.getFirstTarget()!),
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) { () => {
user.turnData.hitsLeft = -1;
user.turnData.hitCount = 0;
user.turnData.extraTurns--;
}
/**
* 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 NumberHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, add another hit
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
user.turnData.hitsLeft = hitCount.value;
}
/**
* 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}
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
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 */
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag);
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT));
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, 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 || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit();
if (hasActiveTargets) {
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
}
user.pushMoveHistory(moveHistoryEntry);
return this.end();
}
/** All move effect attributes are chained together in this array to be applied asynchronously. */
const applyAttrs: Promise<void>[] = [];
const playOnEmptyField = globalScene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
@ -313,7 +313,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's first strike? */ /** Does this phase represent the invoked move's first strike? */
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); const firstHit = user.turnData.hitsLeft === user.turnData.hitCount;
// Only log the move's result on the first strike // Only log the move's result on the first strike
if (firstHit) { if (firstHit) {
@ -363,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's last strike? */ /** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()); const lastHit = user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive();
/** /**
* If the user can change forms by using the invoked move, * If the user can change forms by using the invoked move,
@ -381,43 +381,29 @@ export class MoveEffectPhase extends PokemonPhase {
} }
} }
/** applyFilteredMoveAttrs(
* Create a Promise that applies *all* effects from the invoked move's MoveEffectAttrs. (attr: MoveAttr) =>
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger attr instanceof MoveEffectAttr &&
* type requires different conditions to be met with respect to the move's hit result. attr.trigger === MoveEffectTrigger.PRE_APPLY &&
*/ (!attr.firstHitOnly || firstHit) &&
const k = new Promise<void>((resolve) => { (!attr.lastHitOnly || lastHit) &&
//Start promise chain and apply PRE_APPLY move attributes hitResult !== HitResult.NO_EFFECT,
let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) => user,
attr instanceof MoveEffectAttr target,
&& attr.trigger === MoveEffectTrigger.PRE_APPLY move,
&& (!attr.firstHitOnly || firstHit) );
&& (!attr.lastHitOnly || lastHit)
&& hitResult !== HitResult.NO_EFFECT, user, target, move);
/** Don't complete if the move failed */ if (hitResult !== HitResult.FAIL) {
if (hitResult === HitResult.FAIL) { this.applySelfTargetEffects(user, target, firstHit, lastHit);
return resolve();
}
/** Apply Move/Ability Effects in correct order */
promiseChain = promiseChain
.then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
if (hitResult !== HitResult.NO_EFFECT) { if (hitResult !== HitResult.NO_EFFECT) {
promiseChain this.applyPostApplyEffects(user, target, firstHit, lastHit);
.then(this.applyPostApplyEffects(user, target, firstHit, lastHit)) this.applyHeldItemFlinchCheck(user, target, dealsDamage);
.then(this.applyHeldItemFlinchCheck(user, target, dealsDamage)) this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget);
.then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
.then(() => resolve());
} else { } else {
promiseChain applyMoveAttrs(NoEffectAttr, user, null, move);
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
} }
}); }
applyAttrs.push(k);
} }
// Apply queued phases // Apply queued phases
@ -425,41 +411,35 @@ export class MoveEffectPhase extends PokemonPhase {
globalScene.appendToPhase(queuedPhases, MoveEndPhase); globalScene.appendToPhase(queuedPhases, MoveEndPhase);
} }
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? if (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs(
null; (attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
user,
if (postTarget) { null,
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after move,
applyAttrs[applyAttrs.length - 1].then(() => postTarget); );
} else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget);
}
} }
// Wait for all move effects to finish applying, then end this phase /**
Promise.allSettled(applyAttrs).then(() => { * Remove the target's substitute (if it exists and has expired)
/** * after all targeted effects have applied.
* Remove the target's substitute (if it exists and has expired) * This prevents blocked effects from applying until after this hit resolves.
* after all targeted effects have applied. */
* This prevents blocked effects from applying until after this hit resolves. targets.forEach((target) => {
*/ const substitute = target.getTag(SubstituteTag);
targets.forEach(target => { if (substitute && substitute.hp <= 0) {
const substitute = target.getTag(SubstituteTag); target.lapseTag(BattlerTagType.SUBSTITUTE);
if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE);
}
});
const moveType = user.getMoveType(move, true);
if (move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType);
} }
this.end();
}); });
});
}); const moveType = user.getMoveType(move, true);
if (move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType);
}
this.end();
},
);
} }
public override end(): void { public override end(): void {
@ -500,8 +480,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param lastHit - `true` if this is the last hit in a multi-hit attack * @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> { protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): void {
return () => applyFilteredMoveAttrs((attr: MoveAttr) => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && attr.selfTarget
@ -518,8 +498,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param lastHit - `true` if this is the last hit in a multi-hit attack * @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> { protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): void {
return () => applyFilteredMoveAttrs((attr: MoveAttr) => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY && attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget && !attr.selfTarget
@ -537,8 +517,8 @@ export class MoveEffectPhase extends PokemonPhase {
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> { protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): void {
return applyFilteredMoveAttrs((attr: MoveAttr) => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT && attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.firstHitOnly || firstHit)
@ -554,21 +534,18 @@ export class MoveEffectPhase extends PokemonPhase {
* @param hitResult - The {@linkcode HitResult} of the attempted move * @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call. * @returns a `Promise` intended to be passed into a `then()` call.
*/ */
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> { protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void {
return executeIf(!target.isFainted() || target.canApplyAbility(), () => if (!target.isFainted() || target.canApplyAbility()) {
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult) applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult);
.then(() => {
if (!this.move.getMove().hitsSubstitute(user, target)) { if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target); globalScene.applyShuffledModifiers(EnemyAttackStatusEffectChanceModifier, false, target);
} }
target.lapseTags(BattlerTagLapseType.AFTER_HIT); target.lapseTags(BattlerTagLapseType.AFTER_HIT);
} }
}
})
);
} }
/** /**
@ -583,17 +560,15 @@ export class MoveEffectPhase extends PokemonPhase {
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> { protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean, isProtected: boolean, hitResult: HitResult, firstTarget: boolean): void {
return () => executeIf(!isProtected, () => if (!isProtected) {
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() => this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget);
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() => this.applyOnGetHitAbEffects(user, target, hitResult);
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult);
if (this.move.getMove() instanceof AttackMove) {
if (this.move.getMove() instanceof AttackMove) { globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); }
} }
})
);
} }
/** /**
@ -603,20 +578,18 @@ export class MoveEffectPhase extends PokemonPhase {
* @param dealsDamage - `true` if the attempted move successfully dealt damage * @param dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call. * @returns a function intended to be passed into a `then()` call.
*/ */
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : void {
return () => { if (this.move.getMove().hasAttr(FlinchAttr)) {
if (this.move.getMove().hasAttr(FlinchAttr)) { return;
return; }
}
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false); const flinched = new BooleanHolder(false);
globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) { if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
} }
}; }
} }
/** /**

View File

@ -22,9 +22,8 @@ export class MoveHeaderPhase extends BattlePhase {
super.start(); super.start();
if (this.canMove()) { if (this.canMove()) {
applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove()).then(() => this.end()); applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove());
} else {
this.end();
} }
this.end();
} }
} }

View File

@ -0,0 +1,77 @@
import type { BattlerIndex } from "#app/battle";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { EFFECTIVE_STATS, BATTLE_STATS } from "#enums/stat";
import { PokemonMove } from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { PokemonPhase } from "./pokemon-phase";
/**
* Transforms a Pokemon into another Pokemon on the field.
* Used for Transform (move) and Imposter (ability)
*/
export class PokemonTransformPhase extends PokemonPhase {
protected targetIndex: BattlerIndex;
private playSound: boolean;
constructor(userIndex: BattlerIndex, targetIndex: BattlerIndex, playSound: boolean = false) {
super(userIndex);
this.targetIndex = targetIndex;
this.playSound = playSound;
}
public override start(): void {
const user = this.getPokemon();
const target = globalScene.getField(true).find((p) => p.getBattlerIndex() === this.targetIndex);
if (!target) {
return this.end();
}
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
// Power Trick's effect is removed after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false);
}
// Copy all stat stages
for (const s of BATTLE_STATS) {
user.setStatStage(s, target.getStatStage(s));
}
user.summonData.moveset = target.getMoveset().map((m) => {
if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
} else {
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(Moves.NONE);
}
});
user.summonData.types = target.getTypes();
const promises = [ user.updateInfo() ];
if (this.playSound) {
globalScene.playSound("battle_anims/PRSFX- Transform");
}
promises.push(
user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
// If the new ability activates immediately, it needs to happen after all the transform animations
user.setTempAbility(target.getAbility());
}),
);
Promise.allSettled(promises).then(() => this.end());
}
}

View File

@ -27,12 +27,10 @@ export class PostSummonPhase extends PokemonPhase {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
} }
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon) applyPostSummonAbAttrs(PostSummonAbAttr, pokemon);
.then(() => { const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
this.end(); this.end();
});
} }
} }

View File

@ -0,0 +1,61 @@
import { SwitchType } from "#enums/switch-type";
import { globalScene } from "#app/global-scene";
import type { PartyOption } from "#app/ui/party-ui-handler";
import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { BattlePhase } from "#app/phases/battle-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import type { PlayerPokemon } from "#app/field/pokemon";
/**
* Sets the Party UI and handles the effect of Revival Blessing
* when used by one of the player's Pokemon.
*/
export class RevivalBlessingPhase extends BattlePhase {
constructor(protected user: PlayerPokemon) {
super();
}
public override start(): void {
globalScene.ui.setMode(
Mode.PARTY,
PartyUiMode.REVIVAL_BLESSING,
this.user.getFieldIndex(),
(slotIndex: integer, option: PartyOption) => {
if (slotIndex >= 0 && slotIndex < 6) {
const pokemon = globalScene.getPlayerParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {
return this.end();
}
pokemon.resetTurnData();
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true);
if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1) {
const allyPokemon = this.user.getAlly();
if (slotIndex <= 1) {
// Revived ally pokemon
globalScene.unshiftPhase(
new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, true),
);
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
} else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted
globalScene.unshiftPhase(
new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, true),
);
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
}
}
}
globalScene.ui.setMode(Mode.MESSAGE).then(() => this.end());
},
PartyUiHandler.FilterFainted,
);
}
}

View File

@ -17,17 +17,16 @@ export class RibbonModifierRewardPhase extends ModifierRewardPhase {
doReward(): Promise<void> { doReward(): Promise<void> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier(); const newModifier = this.modifierType.newModifier();
globalScene.addModifier(newModifier).then(() => { globalScene.addModifier(newModifier);
globalScene.playSound("level_up_fanfare"); globalScene.playSound("level_up_fanfare");
globalScene.ui.setMode(Mode.MESSAGE); globalScene.ui.setMode(Mode.MESSAGE);
globalScene.ui.showText(i18next.t("battle:beatModeFirstTime", { globalScene.ui.showText(i18next.t("battle:beatModeFirstTime", {
speciesName: this.species.name, speciesName: this.species.name,
gameMode: globalScene.gameMode.getName(), gameMode: globalScene.gameMode.getName(),
newModifier: newModifier?.type.name newModifier: newModifier?.type.name,
}), null, () => { }), null, () => {
resolve(); resolve();
}, null, true, 1500); }, null, true, 1500);
});
}); });
} }
} }

View File

@ -171,30 +171,21 @@ export class SelectModifierPhase extends BattlePhase {
} }
if (cost && !(modifier.type instanceof RememberMoveModifierType)) { if (cost && !(modifier.type instanceof RememberMoveModifierType)) {
result.then(success => { if (result) {
if (success) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { globalScene.money -= cost;
globalScene.money -= cost; globalScene.updateMoneyText();
globalScene.updateMoneyText(); globalScene.animateMoneyChanged(false);
globalScene.animateMoneyChanged(false);
}
globalScene.playSound("se/buy");
(globalScene.ui.getHandler() as ModifierSelectUiHandler).updateCostText();
} else {
globalScene.ui.playError();
} }
}); globalScene.playSound("se/buy");
} else { (globalScene.ui.getHandler() as ModifierSelectUiHandler).updateCostText();
const doEnd = () => {
globalScene.ui.clearText();
globalScene.ui.setMode(Mode.MESSAGE);
super.end();
};
if (result instanceof Promise) {
result.then(() => doEnd());
} else { } else {
doEnd(); globalScene.ui.playError();
} }
} else {
globalScene.ui.clearText();
globalScene.ui.setMode(Mode.MESSAGE);
super.end();
} }
}; };
@ -304,7 +295,7 @@ export class SelectModifierPhase extends BattlePhase {
); );
} }
addModifier(modifier: Modifier): Promise<boolean> { addModifier(modifier: Modifier): boolean {
return globalScene.addModifier(modifier, false, true); return globalScene.addModifier(modifier, false, true);
} }
} }

View File

@ -391,7 +391,7 @@ describe("Abilities - Unburden", () => {
await game.forceEnemyMove(Moves.THIEF, BattlerIndex.PLAYER); await game.forceEnemyMove(Moves.THIEF, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH); await game.forceEnemyMove(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]);
game.doSelectPartyPokemon(0, "MoveEffectPhase"); game.doSelectPartyPokemon(0, "RevivalBlessingPhase");
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.getPlayerField()[0]).toBe(treecko); expect(game.scene.getPlayerField()[0]).toBe(treecko);

View File

@ -0,0 +1,117 @@
import { BattlerIndex } from "#app/battle";
import { MoveResult } from "#app/field/pokemon";
import { toDmgValue } from "#app/utils";
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 - Revival Blessing", () => {
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
.moveset([ Moves.SPLASH, Moves.REVIVAL_BLESSING, Moves.MEMENTO ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should revive a selected fainted Pokemon when used by the player", async () => {
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.MEMENTO);
game.doSelectPartyPokemon(1, "SwitchPhase");
await game.toNextTurn();
const player = game.scene.getPlayerPokemon()!;
expect(player.species.speciesId).toBe(Species.MAGIKARP);
game.move.select(Moves.REVIVAL_BLESSING);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1, "RevivalBlessingPhase");
await game.phaseInterceptor.to("MoveEndPhase", false);
const revivedPokemon = game.scene.getPlayerParty()[1];
expect(revivedPokemon.status?.effect).toBeFalsy();
expect(revivedPokemon.hp).toBe(Math.floor(revivedPokemon.getMaxHp() / 2));
});
it("should revive a random fainted enemy when used by an enemy Trainer", async () => {
game.override.enemyMoveset(Moves.REVIVAL_BLESSING).startingWave(8);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEndPhase", false);
const revivedPokemon = game.scene.getEnemyParty()[1];
expect(revivedPokemon.status?.effect).toBeFalsy();
expect(revivedPokemon.hp).toBe(Math.floor(revivedPokemon.getMaxHp() / 2));
});
it("should fail when there are no fainted Pokemon to target", async () => {
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.REVIVAL_BLESSING);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase", false);
const player = game.scene.getPlayerPokemon()!;
expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should revive a player pokemon and immediately send it back out if used in the same turn it fainted in doubles", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.SPLASH, Moves.FISSURE ])
.enemyAbility(Abilities.NO_GUARD)
.enemyLevel(100);
await game.classicMode.startBattle([ Species.FEEBAS, Species.MILOTIC, Species.GYARADOS ]);
const feebas = game.scene.getPlayerField()[0];
game.move.select(Moves.SPLASH);
game.move.select(Moves.REVIVAL_BLESSING, 1);
await game.forceEnemyMove(Moves.FISSURE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]);
await game.phaseInterceptor.to("MoveEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(feebas.isFainted()).toBe(true);
game.doSelectPartyPokemon(0, "RevivalBlessingPhase");
await game.toNextTurn();
expect(feebas.isFainted()).toBe(false);
expect(feebas.hp).toBe(toDmgValue(0.5 * feebas.getMaxHp()));
expect(game.scene.getPlayerField()[0]).toBe(feebas);
});
});

View File

@ -375,6 +375,6 @@ describe("Clowning Around - Mystery Encounter", () => {
async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: number, itemType: PokemonHeldItemModifierType) { async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: number, itemType: PokemonHeldItemModifierType) {
const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier;
itemMod.stackCount = stackCount; itemMod.stackCount = stackCount;
await scene.addModifier(itemMod, true, false, false, true); scene.addModifier(itemMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
} }

View File

@ -123,8 +123,6 @@ describe("Dancing Lessons - Mystery Encounter", () => {
partyLead.level = 1000; partyLead.level = 1000;
partyLead.calculateStats(); partyLead.calculateStats();
await runMysteryEncounterToEnd(game, 1, undefined, true); await runMysteryEncounterToEnd(game, 1, undefined, true);
// For some reason updateModifiers breaks in this test and does not resolve promise
vi.spyOn(game.scene, "updateModifiers").mockImplementation(() => new Promise(resolve => resolve()));
await skipBattleRunMysteryEncounterRewardsPhase(game); await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false); await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);

View File

@ -123,7 +123,7 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const amuletCoin = generateModifierType(modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier; const amuletCoin = generateModifierType(modifierTypes.AMULET_COIN)!.newModifier() as MoneyMultiplierModifier;
amuletCoin.stackCount = 5; amuletCoin.stackCount = 5;
await scene.addModifier(amuletCoin, true, false, false, true); scene.addModifier(amuletCoin, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 1); await runMysteryEncounterToEnd(game, 1);
@ -193,7 +193,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!; const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!;
const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
sitrusMod.stackCount = 2; sitrusMod.stackCount = 2;
await scene.addModifier(sitrusMod, true, false, false, true); scene.addModifier(sitrusMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -214,7 +214,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -234,13 +234,13 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const candyJar = generateModifierType(modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier; const candyJar = generateModifierType(modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier;
candyJar.stackCount = 99; candyJar.stackCount = 99;
await scene.addModifier(candyJar, true, false, false, true); scene.addModifier(candyJar, true, false, false, true);
const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!; const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!;
// Sitrus berries on party // Sitrus berries on party
const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
sitrusMod.stackCount = 2; sitrusMod.stackCount = 2;
await scene.addModifier(sitrusMod, true, false, false, true); scene.addModifier(sitrusMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -263,13 +263,13 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; const healingCharm = generateModifierType(modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
healingCharm.stackCount = 3; healingCharm.stackCount = 3;
await scene.addModifier(healingCharm, true, false, false, true); scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Reviver Seed on party lead // Set 1 Reviver Seed on party lead
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -292,7 +292,7 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); const modifier = soulDew.newModifier(scene.getPlayerParty()[0]);
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.phaseInterceptor.to(MysteryEncounterPhase, false);
@ -321,7 +321,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
@ -355,7 +355,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 2; modifier.stackCount = 2;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -376,7 +376,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -396,13 +396,13 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; const healingCharm = generateModifierType(modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
healingCharm.stackCount = 5; healingCharm.stackCount = 5;
await scene.addModifier(healingCharm, true, false, false, true); scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Soul Dew on party lead // Set 1 Soul Dew on party lead
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -425,7 +425,7 @@ describe("Delibird-y - Mystery Encounter", () => {
scene.modifiers = []; scene.modifiers = [];
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!;
const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); const modifier = revSeed.newModifier(scene.getPlayerParty()[0]);
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.phaseInterceptor.to(MysteryEncounterPhase, false);
@ -455,7 +455,7 @@ describe("Delibird-y - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });

View File

@ -224,7 +224,7 @@ describe("Global Trade System - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 2; modifier.stackCount = 2;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
@ -249,7 +249,7 @@ describe("Global Trade System - Mystery Encounter", () => {
const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!;
const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier;
modifier.stackCount = 1; modifier.stackCount = 1;
await scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });

View File

@ -216,11 +216,11 @@ describe("Uncommon Breed - Mystery Encounter", () => {
const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!; const sitrus = generateModifierType(modifierTypes.BERRY, [ BerryType.SITRUS ])!;
const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
sitrusMod.stackCount = 2; sitrusMod.stackCount = 2;
await scene.addModifier(sitrusMod, true, false, false, true); scene.addModifier(sitrusMod, true, false, false, true);
const ganlon = generateModifierType(modifierTypes.BERRY, [ BerryType.GANLON ])!; const ganlon = generateModifierType(modifierTypes.BERRY, [ BerryType.GANLON ])!;
const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier;
ganlonMod.stackCount = 3; ganlonMod.stackCount = 3;
await scene.addModifier(ganlonMod, true, false, false, true); scene.addModifier(ganlonMod, true, false, false, true);
await scene.updateModifiers(true); await scene.updateModifiers(true);
await runMysteryEncounterToEnd(game, 2); await runMysteryEncounterToEnd(game, 2);

View File

@ -91,6 +91,7 @@ export default class GameWrapper {
Pokemon.prototype.updateFusionPalette = () => null; Pokemon.prototype.updateFusionPalette = () => null;
Pokemon.prototype.cry = () => null; Pokemon.prototype.cry = () => null;
Pokemon.prototype.faintCry = (cb) => { if (cb) cb(); }; Pokemon.prototype.faintCry = (cb) => { if (cb) cb(); };
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
} }
setScene(scene: BattleScene) { setScene(scene: BattleScene) {

View File

@ -60,6 +60,7 @@ import { RibbonModifierRewardPhase } from "#app/phases/ribbon-modifier-reward-ph
import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase"; import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase";
import { UnlockPhase } from "#app/phases/unlock-phase"; import { UnlockPhase } from "#app/phases/unlock-phase";
import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; import { PostGameOverPhase } from "#app/phases/post-game-over-phase";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
export interface PromptHandler { export interface PromptHandler {
phaseTarget?: string; phaseTarget?: string;
@ -126,7 +127,8 @@ type PhaseClass =
| typeof EncounterPhase | typeof EncounterPhase
| typeof GameOverPhase | typeof GameOverPhase
| typeof UnlockPhase | typeof UnlockPhase
| typeof PostGameOverPhase; | typeof PostGameOverPhase
| typeof RevivalBlessingPhase;
type PhaseString = type PhaseString =
| "LoginPhase" | "LoginPhase"
@ -185,7 +187,8 @@ type PhaseString =
| "EncounterPhase" | "EncounterPhase"
| "GameOverPhase" | "GameOverPhase"
| "UnlockPhase" | "UnlockPhase"
| "PostGameOverPhase"; | "PostGameOverPhase"
| "RevivalBlessingPhase";
type PhaseInterceptorPhase = PhaseClass | PhaseString; type PhaseInterceptorPhase = PhaseClass | PhaseString;
@ -269,6 +272,7 @@ export default class PhaseInterceptor {
[ GameOverPhase, this.startPhase ], [ GameOverPhase, this.startPhase ],
[ UnlockPhase, this.startPhase ], [ UnlockPhase, this.startPhase ],
[ PostGameOverPhase, this.startPhase ], [ PostGameOverPhase, this.startPhase ],
[ RevivalBlessingPhase, this.startPhase ],
]; ];
private endBySetMode = [ private endBySetMode = [
@ -511,11 +515,11 @@ export default class PhaseInterceptor {
if (expireFn) { if (expireFn) {
this.prompts.shift(); this.prompts.shift();
} else if ( } else if (
currentMode === actionForNextPrompt.mode currentMode === actionForNextPrompt.mode &&
&& currentPhase === actionForNextPrompt.phaseTarget currentPhase === actionForNextPrompt.phaseTarget &&
&& currentHandler.active currentHandler.active &&
&& (!actionForNextPrompt.awaitingActionInput (!actionForNextPrompt.awaitingActionInput ||
|| (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput)) (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))
) { ) {
const prompt = this.prompts.shift(); const prompt = this.prompts.shift();
if (prompt?.callback) { if (prompt?.callback) {