[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 resolve(false); return 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 => { const overridden = args[0] as Utils.BooleanHolder;
if (args.length < 2 || !args[1]) { const virtual = args[1] as boolean;
new MoveChargeAnim(this.chargeAnim, move.id, user).play(false, () => {
(args[0] as Utils.BooleanHolder).value = 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))); globalScene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex()); globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
resolve(true);
});
} else { } else {
globalScene.ui.showText(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }), null, () => resolve(true)); 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().then(() => { user.updateInfo();
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);
} }
resolve(ret); return 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 resolve(false); return false;
} }
const infoUpdates: Promise<void>[] = [];
const hpValue = Math.floor((target.hp + user.hp) / 2); const hpValue = Math.floor((target.hp + user.hp) / 2);
if (user.hp < hpValue) { [ user, target ].forEach((p) => {
const healing = user.heal(hpValue - user.hp); if (p.hp < hpValue) {
const healing = p.heal(hpValue - p.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,22 +6014,17 @@ 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.getPlayerParty().findIndex(p => p.isFainted()) > -1) { globalScene.unshiftPhase(new RevivalBlessingPhase(user));
(user as PlayerPokemon).revivalBlessing().then(() => { return true;
resolve(true); } else if (user instanceof EnemyPokemon && user.hasTrainer() && globalScene.getEnemyParty().findIndex((p) => p.isFainted() && !p.isBoss()) > -1) {
}); // If used by an enemy trainer with at least one fainted non-boss Pokemon, this
// If user is enemy, checks that it is a trainer, and it has fainted non-boss pokemon in party // revives one of said Pokemon selected at random.
} else if (user instanceof EnemyPokemon const faintedPokemon = globalScene.getEnemyParty().filter((p) => p.isFainted() && !p.isBoss());
&& user.hasTrainer()
&& globalScene.getEnemyParty().findIndex(p => p.isFainted() && !p.isBoss()) > -1) {
// Selects a random fainted pokemon
const faintedPokemon = globalScene.getEnemyParty().filter(p => p.isFainted() && !p.isBoss());
const pokemon = faintedPokemon[user.randSeedInt(faintedPokemon.length)]; const pokemon = faintedPokemon[user.randSeedInt(faintedPokemon.length)];
const slotIndex = globalScene.getEnemyParty().findIndex(p => pokemon.id === p.id); const slotIndex = globalScene.getEnemyParty().findIndex((p) => pokemon.id === p.id);
pokemon.resetStatus(); pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp())); pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true); globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
@ -6052,16 +6037,21 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false)); globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false));
} }
} }
resolve(true); return true;
} else {
globalScene.queueMessage(i18next.t("battle:attackFailed"));
resolve(false);
} }
}); 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,8 +6952,7 @@ 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
@ -7098,12 +7084,9 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
} }
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true }); user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
globalScene.unshiftPhase(new LoadMoveAnimPhase(moveId));
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true)); globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true));
initMoveAnim(moveId).then(() => { return true;
loadMoveAnimAssets([ moveId ], 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) {
@ -4731,8 +4693,7 @@ 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;
@ -4741,7 +4702,7 @@ export class PlayerPokemon extends Pokemon {
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;
} }
@ -4750,7 +4711,7 @@ export class PlayerPokemon extends Pokemon {
// 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();
@ -4774,24 +4735,17 @@ export class PlayerPokemon extends Pokemon {
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[];
const transferModifiers: Promise<boolean>[] = [];
for (const modifier of fusedPartyMemberHeldModifiers) { for (const modifier of fusedPartyMemberHeldModifiers) {
transferModifiers.push(globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false)); globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false);
} }
Promise.allSettled(transferModifiers).then(() => { globalScene.updateModifiers(true, true);
globalScene.updateModifiers(true, true).then(() => {
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex); globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id))); pokemon.getMoveset(true).map((m: PokemonMove) => globalScene.unshiftPhase(new LearnMovePhase(newPartyMemberIndex, m.getMove().id)));
pokemon.destroy(); pokemon.destroy();
this.updateFusionPalette(); 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,11 +44,10 @@ 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();
}); });
});
} }
/** Checks the move's instant charge conditions, then ends this phase. */ /** Checks the move's instant charge conditions, then ends this phase. */

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,7 +143,8 @@ 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 other effects were overriden, stop this phase before they can be applied
if (overridden.value) { if (overridden.value) {
return this.end(); return this.end();
@ -217,12 +218,11 @@ export class MoveEffectPhase extends PokemonPhase {
return this.end(); 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; const playOnEmptyField = globalScene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target // Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => { 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,26 +411,21 @@ 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) * Remove the target's substitute (if it exists and has expired)
* after all targeted effects have applied. * after all targeted effects have applied.
* This prevents blocked effects from applying until after this hit resolves. * This prevents blocked effects from applying until after this hit resolves.
*/ */
targets.forEach(target => { targets.forEach((target) => {
const substitute = target.getTag(SubstituteTag); const substitute = target.getTag(SubstituteTag);
if (substitute && substitute.hp <= 0) { if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE); target.lapseTag(BattlerTagType.SUBSTITUTE);
@ -457,9 +438,8 @@ export class MoveEffectPhase extends PokemonPhase {
} }
this.end(); 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,10 +534,9 @@ 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) {
@ -566,9 +545,7 @@ export class MoveEffectPhase extends PokemonPhase {
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,8 +578,7 @@ 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;
} }
@ -616,7 +590,6 @@ export class MoveEffectPhase extends PokemonPhase {
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,8 +171,7 @@ 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();
@ -183,18 +182,10 @@ export class SelectModifierPhase extends BattlePhase {
} else { } else {
globalScene.ui.playError(); globalScene.ui.playError();
} }
});
} else { } else {
const doEnd = () => {
globalScene.ui.clearText(); globalScene.ui.clearText();
globalScene.ui.setMode(Mode.MESSAGE); globalScene.ui.setMode(Mode.MESSAGE);
super.end(); super.end();
};
if (result instanceof Promise) {
result.then(() => doEnd());
} else {
doEnd();
}
} }
}; };
@ -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) {