From 6a47cb01da5c1725ef58e5d81bcc7ac9054bb57c Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sat, 20 Sep 2025 17:39:24 -0700 Subject: [PATCH 1/3] Update interactions to use generator --- src/battle-scene.ts | 4 ++ src/data/abilities/ability.ts | 28 ++++++------- src/data/arena-tag.ts | 54 ++++++++++++------------- src/data/moves/move.ts | 16 +++++--- src/field/arena.ts | 43 +++++++++----------- src/field/pokemon.ts | 51 ++++++++++++----------- src/modifier/modifier.ts | 4 +- src/phases/attempt-run-phase.ts | 10 ++--- src/phases/check-status-effect-phase.ts | 7 ++-- src/phases/faint-phase.ts | 5 ++- src/phases/field-phase.ts | 5 ++- src/phases/move-phase.ts | 16 ++++---- src/phases/mystery-encounter-phases.ts | 10 +++-- src/phases/post-summon-phase.ts | 3 +- src/phases/stat-stage-change-phase.ts | 2 +- src/phases/switch-summon-phase.ts | 8 ++-- src/utils/speed-order.ts | 11 +++-- 17 files changed, 146 insertions(+), 131 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 289c9a8f051..866ca37d053 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -802,6 +802,10 @@ export class BattleScene extends SceneBase { * @param activeOnly - Whether to consider only active pokemon (as described by {@linkcode Pokemon.isActive()}); default `false`. * If `true`, will also remove all `null` values from the array. * @returns An array of {@linkcode Pokemon}, as described above. + * + * @remarks + * This should *only* be used in instances where speed order is not relevant. + * If speed order matters, use {@linkcode inSpeedOrder}. */ public getField(activeOnly = false): Pokemon[] { const ret: Pokemon[] = new Array(4).fill(null); diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index f6494548b99..5d70ed191ca 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -74,6 +74,7 @@ import { randSeedItem, toDmgValue, } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -2769,7 +2770,7 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { return; } - for (const opponent of pokemon.getOpponents()) { + for (const opponent of pokemon.getOpponentsGenerator()) { const cancelled = new BooleanHolder(false); if (this.intimidate) { const params: AbAttrParamsWithCancel = { pokemon: opponent, cancelled, simulated }; @@ -3079,16 +3080,12 @@ export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAtt if (simulated) { return; } - const party = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - const allowedParty = party.filter(p => p.isAllowedInBattle()); - for (const pokemon of allowedParty) { - if (pokemon.status && this.statusEffect.includes(pokemon.status.effect)) { - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)), - ); - pokemon.resetStatus(false); - pokemon.updateInfo(); + for (const p of pokemon.getAlliedField()) { + if (p.status && this.statusEffect.includes(p.status.effect)) { + globalScene.phaseManager.queueMessage(getStatusEffectHealText(p.status.effect, getPokemonNameWithAffix(p))); + p.resetStatus(false); + p.updateInfo(); } } } @@ -4302,7 +4299,7 @@ export class FriskAbAttr extends PostSummonAbAttr { override apply({ simulated, pokemon }: AbAttrBaseParams): void { if (!simulated) { - for (const opponent of pokemon.getOpponents()) { + for (const opponent of pokemon.getOpponentsGenerator()) { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:frisk", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -4869,7 +4866,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { return; } - for (const opp of pokemon.getOpponents()) { + for (const opp of pokemon.getOpponentsGenerator()) { if ((opp.status?.effect !== StatusEffect.SLEEP && !opp.hasAbility(AbilityId.COMATOSE)) || opp.switchOutStatus) { continue; } @@ -5420,10 +5417,9 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { } const cancelled = new BooleanHolder(false); - // TODO: This should be in speed order - globalScene - .getField(true) - .forEach(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", { pokemon: p, cancelled, simulated })); + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { + applyAbAttrs("FieldPreventExplosiveMovesAbAttr", { pokemon: p, cancelled, simulated }); + } if (cancelled.value) { return false; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fd64e271758..dffca78e77a 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -77,6 +77,7 @@ import type { } from "#types/arena-tags"; import type { Mutable } from "#types/type-helpers"; import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import i18next from "i18next"; /** Interface containing the serializable fields of ArenaTagData. */ @@ -187,7 +188,7 @@ export abstract class ArenaTag implements BaseArenaTag { /** * Helper function that retrieves the Pokemon affected - * @returns list of PlayerPokemon or EnemyPokemon on the field + * @returns A list of PlayerPokemon or EnemyPokemon on the field, not in speed order */ public getAffectedPokemon(): Pokemon[] { switch (this.side) { @@ -1236,7 +1237,7 @@ export class GravityTag extends SerializableArenaTag { onAdd(_arena: Arena): void { globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd")); - globalScene.getField(true).forEach(pokemon => { + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { if (pokemon !== null) { pokemon.removeTag(BattlerTagType.FLOATING); pokemon.removeTag(BattlerTagType.TELEKINESIS); @@ -1244,7 +1245,7 @@ export class GravityTag extends SerializableArenaTag { pokemon.addTag(BattlerTagType.INTERRUPTED); } } - }); + } } onRemove(_arena: Arena): void { @@ -1279,9 +1280,7 @@ class TailwindTag extends SerializableArenaTag { ); } - const field = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - - for (const pokemon of field) { + for (const pokemon of source.getAlliedField()) { // Apply the CHARGED tag to party members with the WIND_POWER ability // TODO: This should not be handled here if (pokemon.hasAbility(AbilityId.WIND_POWER) && !pokemon.getTag(BattlerTagType.CHARGED)) { @@ -1394,27 +1393,26 @@ class FireGrassPledgeTag extends SerializableArenaTag { } override lapse(arena: Arena): boolean { - const field: Pokemon[] = - this.side === ArenaTagSide.PLAYER ? globalScene.getPlayerField() : globalScene.getEnemyField(); + for (const pokemon of inSpeedOrder(this.side)) { + if (pokemon.isOfType(PokemonType.FIRE) || pokemon.switchOutStatus) { + continue; + } - field - .filter(pokemon => !pokemon.isOfType(PokemonType.FIRE) && !pokemon.switchOutStatus) - .forEach(pokemon => { - // "{pokemonNameWithAffix} was hurt by the sea of fire!" - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:fireGrassPledgeLapse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - // TODO: Replace this with a proper animation - globalScene.phaseManager.unshiftNew( - "CommonAnimPhase", - pokemon.getBattlerIndex(), - pokemon.getBattlerIndex(), - CommonAnim.MAGMA_STORM, - ); - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); - }); + // "{pokemonNameWithAffix} was hurt by the sea of fire!" + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:fireGrassPledgeLapse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + ); + // TODO: Replace this with a proper animation + globalScene.phaseManager.unshiftNew( + "CommonAnimPhase", + pokemon.getBattlerIndex(), + pokemon.getBattlerIndex(), + CommonAnim.MAGMA_STORM, + ); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); + } return super.lapse(arena); } @@ -1532,7 +1530,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { if (pokemon) { this.playActivationMessage(pokemon); - for (const fieldPokemon of globalScene.getField(true)) { + for (const fieldPokemon of inSpeedOrder(ArenaTagSide.BOTH)) { if (fieldPokemon && fieldPokemon.id !== pokemon.id) { // TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing // the appropriate attributes (preLEaveField and IllusionBreak) @@ -1573,7 +1571,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { globalScene.phaseManager.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove")); } - for (const pokemon of globalScene.getField(true)) { + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { // There is only one pokemon with this attr on the field on removal, so its abilities are already active if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) { [true, false].forEach(passive => { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 075876d8ddd..77f2af29398 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -94,6 +94,7 @@ import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; +import { inSpeedOrder } from "#utils/speed-order-generator"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -859,8 +860,9 @@ export abstract class Move implements Localizable { aura.apply({pokemon: source, simulated, opponent: target, move: this, power}); } - const alliedField: Pokemon[] = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - alliedField.forEach(p => applyAbAttrs("UserFieldMoveTypePowerBoostAbAttr", {pokemon: p, opponent: target, move: this, simulated, power})); + for (const p of source.getAlliedField()) { + applyAbAttrs("UserFieldMoveTypePowerBoostAbAttr", {pokemon: p, opponent: target, move: this, simulated, power}); + } power.value *= typeChangeMovePowerMultiplier.value; @@ -5948,8 +5950,10 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr { return false; } - globalScene.getField(true).forEach(pokemon => - pokemon.findAndRemoveTags(tag => tag.tagType === BattlerTagType.SUBSTITUTE)); + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + pokemon.findAndRemoveTags(tag => tag.tagType === BattlerTagType.SUBSTITUTE); + } + return true; } } @@ -7955,7 +7959,9 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => { // temporary workaround to prevent displaying the message during enemy command phase // TODO: either move this, or make the move condition func have a `simulated` param const simulated = globalScene.phaseManager.getCurrentPhase()?.is('EnemyCommandPhase'); - globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled, simulated})); + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { + applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled, simulated}); + } // Queue a message if an ability prevented usage of the move if (!simulated && cancelled.value) { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name })); diff --git a/src/field/arena.ts b/src/field/arena.ts index 3e214ff1ea7..3fd79006f1a 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -38,6 +38,7 @@ import type { Move } from "#moves/move"; import type { AbstractConstructor } from "#types/type-helpers"; import { type Constructor, NumberHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { inSpeedOrder } from "#utils/speed-order-generator"; export class Arena { public biomeType: BiomeId; @@ -356,15 +357,12 @@ export class Arena { globalScene.phaseManager.queueMessage(getWeatherClearMessage(oldWeatherType)!); // TODO: is this bang correct? } - globalScene - .getField(true) - .filter(p => p.isOnField()) - .map(pokemon => { - pokemon.findAndRemoveTags( - t => "weatherTypes" in t && !(t.weatherTypes as WeatherType[]).find(t => t === weather), - ); - applyAbAttrs("PostWeatherChangeAbAttr", { pokemon, weather }); - }); + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + pokemon.findAndRemoveTags( + tag => "weatherTypes" in tag && !(tag.weatherTypes as WeatherType[]).find(t => t === weather), + ); + applyAbAttrs("PostWeatherChangeAbAttr", { pokemon, weather }); + } return true; } @@ -374,7 +372,7 @@ export class Arena { * @param source - The Pokemon causing the changes by removing itself from the field */ triggerWeatherBasedFormChanges(source?: Pokemon): void { - globalScene.getField(true).forEach(p => { + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { // TODO - This is a bandaid. Abilities leaving the field needs a better approach than // calling this method for every switch out that happens if (p === source) { @@ -386,23 +384,23 @@ export class Arena { if (isCastformWithForecast || isCherrimWithFlowerGift) { globalScene.triggerPokemonFormChange(p, SpeciesFormChangeWeatherTrigger); } - }); + } } /** * Function to trigger all weather based form changes back into their normal forms */ triggerWeatherBasedFormChangesToNormal(): void { - globalScene.getField(true).forEach(p => { + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST, false, true) && p.species.speciesId === SpeciesId.CASTFORM; const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT, false, true) && p.species.speciesId === SpeciesId.CHERRIM; if (isCastformWithForecast || isCherrimWithFlowerGift) { - return globalScene.triggerPokemonFormChange(p, SpeciesFormChangeRevertWeatherFormTrigger); + globalScene.triggerPokemonFormChange(p, SpeciesFormChangeRevertWeatherFormTrigger); } - }); + } } /** Returns whether or not the terrain can be set to {@linkcode terrain} */ @@ -451,16 +449,13 @@ export class Arena { globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType)); } - globalScene - .getField(true) - .filter(p => p.isOnField()) - .map(pokemon => { - pokemon.findAndRemoveTags( - t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain), - ); - applyAbAttrs("PostTerrainChangeAbAttr", { pokemon, terrain }); - applyAbAttrs("TerrainEventTypeChangeAbAttr", { pokemon }); - }); + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + pokemon.findAndRemoveTags( + t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain), + ); + applyAbAttrs("PostTerrainChangeAbAttr", { pokemon, terrain }); + applyAbAttrs("TerrainEventTypeChangeAbAttr", { pokemon }); + } return true; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ec813e52e56..0e1e1a93c5e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -172,6 +172,7 @@ import { } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities"; import i18next from "i18next"; import Phaser from "phaser"; @@ -2347,15 +2348,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Holds whether the pokemon is trapped due to an ability */ const trapped = new BooleanHolder(false); - /** - * Contains opposing Pokemon (Enemy/Player Pokemon) depending on perspective - * Afterwards, it filters out Pokemon that have been switched out of the field so trapped abilities/moves do not trigger - */ - const opposingFieldUnfiltered = this.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField(); - const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false); - - for (const opponent of opposingField) { - applyAbAttrs("CheckTrappedAbAttr", { pokemon: opponent, trapped, opponent: this, simulated }, trappedAbMessages); + for (const opponent of inSpeedOrder(this.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER)) { + if (opponent.switchOutStatus === false) { + applyAbAttrs( + "CheckTrappedAbAttr", + { pokemon: opponent, trapped, opponent: this, simulated }, + trappedAbMessages, + ); + } } const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -2465,15 +2465,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } if (!cancelledHolder.value) { - const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - defendingSidePlayField.forEach(p => + for (const p of this.getAlliedField()) { applyAbAttrs("FieldPriorityMoveImmunityAbAttr", { pokemon: p, opponent: source, move, cancelled: cancelledHolder, - }), - ); + }); + } } } @@ -3244,6 +3243,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } + public getOpponentsGenerator(): Generator { + return inSpeedOrder(this.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER); + } + getOpponentDescriptor(): string { return this.isPlayer() ? i18next.t("arenaTag:opposingTeam") : i18next.t("arenaTag:yourTeam"); } @@ -3255,10 +3258,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Gets the Pokémon on the allied field. * - * @returns An array of Pokémon on the allied field. + * @returns An generator of Pokémon on the allied field in speed order. */ - getAlliedField(): Pokemon[] { - return this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + getAlliedField(): Generator { + return inSpeedOrder(this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); } /** @@ -4022,16 +4025,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const cancelled = new BooleanHolder(false); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: stubTag, cancelled, simulated: true }); - const userField = this.getAlliedField(); - userField.forEach(pokemon => + for (const pokemon of this.getAlliedField()) { applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", { pokemon, tag: stubTag, cancelled, simulated: true, target: this, - }), - ); + }); + } return !cancelled.value; } @@ -5576,10 +5578,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { leaveField(clearEffects = true, hideInfo = true, destroy = false) { this.resetSprite(); this.resetTurnData(); - globalScene - .getField(true) - .filter(p => p !== this) - .forEach(p => p.removeTagsBySourceId(this.id)); + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { + if (p !== this) { + p.removeTagsBySourceId(this.id); + } + } if (clearEffects) { this.destroySubstitute(); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 19ddc77d436..c95c349e65f 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1915,9 +1915,7 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { // Remove the Pokemon's FAINT status pokemon.resetStatus(true, false, true, false); - // Reapply Commander on the Pokemon's side of the field, if applicable - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - for (const p of field) { + for (const p of pokemon.getAlliedField()) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); } return true; diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index e8212a27243..c43c92ea860 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -1,10 +1,12 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { FieldPhase } from "#phases/field-phase"; import { NumberHolder } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import i18next from "i18next"; export class AttemptRunPhase extends FieldPhase { @@ -15,16 +17,14 @@ export class AttemptRunPhase extends FieldPhase { // Increment escape attempts count on entry const currentAttempts = globalScene.currentBattle.escapeAttempts++; - - const activePlayerField = globalScene.getPlayerField(true); const enemyField = globalScene.getEnemyField(); const escapeRoll = globalScene.randBattleSeedInt(100); const escapeChance = new NumberHolder(this.calculateEscapeChance(currentAttempts)); - activePlayerField.forEach(pokemon => { + for (const pokemon of inSpeedOrder(ArenaTagSide.PLAYER)) { applyAbAttrs("RunSuccessAbAttr", { pokemon, chance: escapeChance }); - }); + } if (escapeRoll < escapeChance.value) { enemyField.forEach(pokemon => applyAbAttrs("PreLeaveFieldAbAttr", { pokemon })); @@ -56,7 +56,7 @@ export class AttemptRunPhase extends FieldPhase { globalScene.phaseManager.pushNew("NewBattlePhase"); } else { - activePlayerField.forEach(p => { + globalScene.getPlayerField(true).forEach(p => { p.turnData.failedRunAway = true; }); diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index 5955cd42c55..6a810d68c23 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,13 +1,14 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { inSpeedOrder } from "#utils/speed-order-generator"; export class CheckStatusEffectPhase extends Phase { public readonly phaseName = "CheckStatusEffectPhase"; start() { - const field = globalScene.getField(); - for (const p of field) { - if (p?.status?.isPostTurn()) { + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { + if (p.status?.isPostTurn()) { globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex()); } } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 821d16c6546..4762d39420c 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -5,6 +5,7 @@ import { FRIENDSHIP_LOSS_FROM_FAINT } from "#balance/starters"; import { allMoves } from "#data/data-lists"; import { battleSpecDialogue } from "#data/dialogue"; import { SpeciesFormChangeActiveTrigger } from "#data/form-change-triggers"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { BattleSpec } from "#enums/battle-spec"; import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; @@ -17,6 +18,7 @@ import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { PokemonInstantReviveModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { PokemonPhase } from "#phases/pokemon-phase"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import i18next from "i18next"; export class FaintPhase extends PokemonPhase { @@ -126,8 +128,7 @@ export class FaintPhase extends PokemonPhase { applyAbAttrs("PostFaintAbAttr", { pokemon }); } - const alivePlayField = globalScene.getField(true); - for (const p of alivePlayField) { + for (const p of inSpeedOrder(ArenaTagSide.BOTH)) { applyAbAttrs("PostKnockOutAbAttr", { pokemon: p, victim: pokemon }); } if (pokemon.turnData.attacksReceived?.length > 0) { diff --git a/src/phases/field-phase.ts b/src/phases/field-phase.ts index 99de3d9fcf5..03b9c63a7cd 100644 --- a/src/phases/field-phase.ts +++ b/src/phases/field-phase.ts @@ -1,12 +1,13 @@ -import { globalScene } from "#app/global-scene"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; +import { inSpeedOrder } from "#utils/speed-order-generator"; type PokemonFunc = (pokemon: Pokemon) => void; export abstract class FieldPhase extends BattlePhase { executeForAll(func: PokemonFunc): void { - for (const pokemon of globalScene.getField(true)) { + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { func(pokemon); } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5e85401db77..0de9b6f0d31 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -10,6 +10,7 @@ import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/st import { getTerrainBlockMessage } from "#data/terrain"; import { getWeatherBlockMessage } from "#data/weather"; import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -29,6 +30,7 @@ import type { PokemonMove } from "#moves/pokemon-move"; import type { TurnMove } from "#types/turn-move"; import { NumberHolder } from "#utils/common"; import { enumValueToKey } from "#utils/enums"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import i18next from "i18next"; export class MovePhase extends PokemonPhase { @@ -380,9 +382,9 @@ export class MovePhase extends PokemonPhase { // TODO: This needs to go at the end of `MoveEffectPhase` to check move results const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const; if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) { - globalScene.getField(true).forEach(pokemon => { + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: this.pokemon, targets: this.targets }); - }); + } } } @@ -510,22 +512,22 @@ export class MovePhase extends PokemonPhase { const redirectTarget = new NumberHolder(currentTarget); // check move redirection abilities of every pokemon *except* the user. - globalScene - .getField(true) - .filter(p => p !== this.pokemon) - .forEach(pokemon => { + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + if (pokemon !== this.pokemon) { applyAbAttrs("RedirectMoveAbAttr", { pokemon, moveId: this.move.moveId, targetIndex: redirectTarget, sourcePokemon: this.pokemon, }); - }); + } + } /** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */ let redirectedByAbility = currentTarget !== redirectTarget.value; // check for center-of-attention tags (note that this will override redirect abilities) + // TODO The target of redirection should be the first viable pokemon that used a redirection move in the turn this.pokemon.getOpponents(true).forEach(p => { const redirectTag = p.getTag(CenterOfAttentionTag); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index bb3f4a92033..eac85f88eee 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; import { getCharVariantFromDialogue } from "#data/dialogue"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { BattleSpec } from "#enums/battle-spec"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -15,6 +16,7 @@ import { transitionMysteryEncounterIntroVisuals } from "#mystery-encounters/enco import type { MysteryEncounterOption, OptionPhaseCallback } from "#mystery-encounters/mystery-encounter-option"; import { SeenEncounterData } from "#mystery-encounters/mystery-encounter-save-data"; import { randSeedItem } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import i18next from "i18next"; /** @@ -216,7 +218,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { // Lapse any residual flinches/endures but ignore all other turn-end battle tags const includedLapseTags = [BattlerTagType.FLINCHED, BattlerTagType.ENDURING]; - globalScene.getField(true).forEach(pokemon => { + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { const tags = pokemon.summonData.tags; tags .filter( @@ -229,7 +231,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { t.onRemove(pokemon); tags.splice(tags.indexOf(t), 1); }); - }); + } // Remove any status tick phases globalScene.phaseManager.removeAllPhasesOfType("PostTurnStatusEffectPhase"); @@ -428,7 +430,9 @@ export class MysteryEncounterBattlePhase extends Phase { } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { - globalScene.getPlayerField().forEach(pokemon => pokemon.lapseTag(BattlerTagType.COMMANDED)); + for (const pokemon of inSpeedOrder(ArenaTagSide.PLAYER)) { + pokemon.lapseTag(BattlerTagType.COMMANDED); + } globalScene.phaseManager.pushNew("ReturnPhase", 1); } globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 136f2fbd601..70a45ad802c 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -39,8 +39,7 @@ export class PostSummonPhase extends PokemonPhase { ) { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - const field = pokemon.isPlayer() ? globalScene.getPlayerField(true) : globalScene.getEnemyField(true); - for (const p of field) { + for (const p of pokemon.getAlliedField()) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); } diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 3c2d1cb5fad..0684ff63469 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -210,7 +210,7 @@ export class StatStageChangePhase extends PokemonPhase { } if (stages.value > 0 && this.canBeCopied) { - for (const opponent of pokemon.getOpponents()) { + for (const opponent of pokemon.getOpponentsGenerator()) { applyAbAttrs("StatStageChangeCopyAbAttr", { pokemon: opponent, stats: this.stats, numStages: stages.value }); } } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 8cc7843b55f..9ccc3cc573e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -5,12 +5,14 @@ import { SubstituteTag } from "#data/battler-tags"; import { allMoves } from "#data/data-lists"; import { SpeciesFormChangeActiveTrigger } from "#data/form-change-triggers"; import { getPokeballTintColor } from "#data/pokeball"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { Command } from "#enums/command"; import { SwitchType } from "#enums/switch-type"; import { TrainerSlot } from "#enums/trainer-slot"; import type { Pokemon } from "#field/pokemon"; import { SwitchEffectTransferModifier } from "#modifiers/modifier"; import { SummonPhase } from "#phases/summon-phase"; +import { inSpeedOrder } from "#utils/speed-order-generator"; import i18next from "i18next"; export class SwitchSummonPhase extends SummonPhase { @@ -69,9 +71,9 @@ export class SwitchSummonPhase extends SummonPhase { } const pokemon = this.getPokemon(); - (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon => - enemyPokemon.removeTagsBySourceId(pokemon.id), - ); + for (const enemyPokemon of inSpeedOrder(this.player ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER)) { + enemyPokemon.removeTagsBySourceId(pokemon.id); + } if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { const substitute = pokemon.getTag(SubstituteTag); diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index 1d894369bb3..87cffbde4a7 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -1,4 +1,4 @@ -import { Pokemon } from "#app/field/pokemon"; +import type { Pokemon } from "#app/field/pokemon"; import { globalScene } from "#app/global-scene"; import { BooleanHolder, randSeedShuffle } from "#app/utils/common"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -38,11 +38,16 @@ function shufflePokemonList(pokemonList: T[]): T return pokemonList; } +/** Type guard for {@linkcode sortBySpeed} to avoid importing {@linkcode Pokemon} */ +function isPokemon(p: Pokemon | hasPokemon): p is Pokemon { + return typeof (p as hasPokemon).getPokemon !== "function"; +} + /** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ function sortBySpeed(pokemonList: T[]): void { pokemonList.sort((a, b) => { - const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); - const bSpeed = (b instanceof Pokemon ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); + const aSpeed = (isPokemon(a) ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); + const bSpeed = (isPokemon(b) ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); return bSpeed - aSpeed; }); From ce8bc325f6e4d9cbe9f88737e73294d3130ed14e Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sat, 20 Sep 2025 18:07:19 -0700 Subject: [PATCH 2/3] Fix triggerWeatherBasedFormChanges --- src/field/arena.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/field/arena.ts b/src/field/arena.ts index 3fd79006f1a..19f18ce0423 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -376,7 +376,7 @@ export class Arena { // TODO - This is a bandaid. Abilities leaving the field needs a better approach than // calling this method for every switch out that happens if (p === source) { - return; + continue; } const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM; const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM; From ab58381c5097e6d177af50d1cc8693242c3c3445 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sat, 20 Sep 2025 18:10:49 -0700 Subject: [PATCH 3/3] Update documentation --- src/data/abilities/ability.ts | 2 +- src/data/arena-tag.ts | 2 +- src/data/moves/move.ts | 2 +- src/field/pokemon.ts | 17 +++++++++-------- src/modifier/modifier.ts | 2 +- src/phases/post-summon-phase.ts | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 5d70ed191ca..37f5b22d007 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -3081,7 +3081,7 @@ export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAtt return; } - for (const p of pokemon.getAlliedField()) { + for (const p of pokemon.getAlliesGenerator()) { if (p.status && this.statusEffect.includes(p.status.effect)) { globalScene.phaseManager.queueMessage(getStatusEffectHealText(p.status.effect, getPokemonNameWithAffix(p))); p.resetStatus(false); diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index dffca78e77a..16843884bd6 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1280,7 +1280,7 @@ class TailwindTag extends SerializableArenaTag { ); } - for (const pokemon of source.getAlliedField()) { + for (const pokemon of source.getAlliesGenerator()) { // Apply the CHARGED tag to party members with the WIND_POWER ability // TODO: This should not be handled here if (pokemon.hasAbility(AbilityId.WIND_POWER) && !pokemon.getTag(BattlerTagType.CHARGED)) { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 77f2af29398..0fb8cb4968b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -860,7 +860,7 @@ export abstract class Move implements Localizable { aura.apply({pokemon: source, simulated, opponent: target, move: this, power}); } - for (const p of source.getAlliedField()) { + for (const p of source.getAlliesGenerator()) { applyAbAttrs("UserFieldMoveTypePowerBoostAbAttr", {pokemon: p, opponent: target, move: this, simulated, power}); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0e1e1a93c5e..13390d4e4fa 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2465,7 +2465,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } if (!cancelledHolder.value) { - for (const p of this.getAlliedField()) { + for (const p of this.getAlliesGenerator()) { applyAbAttrs("FieldPriorityMoveImmunityAbAttr", { pokemon: p, opponent: source, @@ -3233,7 +3233,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Returns the pokemon that oppose this one and are active + * Returns the pokemon that oppose this one and are active in non-speed order * * @param onField - whether to also check if the pokemon is currently on the field (defaults to true) */ @@ -3243,6 +3243,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } + /** + * @returns A generator of pokemon that oppose this one in speed order + */ public getOpponentsGenerator(): Generator { return inSpeedOrder(this.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER); } @@ -3256,11 +3259,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the Pokémon on the allied field. - * * @returns An generator of Pokémon on the allied field in speed order. */ - getAlliedField(): Generator { + getAlliesGenerator(): Generator { return inSpeedOrder(this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); } @@ -4025,7 +4026,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const cancelled = new BooleanHolder(false); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: stubTag, cancelled, simulated: true }); - for (const pokemon of this.getAlliedField()) { + for (const pokemon of this.getAlliesGenerator()) { applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", { pokemon, tag: stubTag, @@ -4067,7 +4068,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - for (const pokemon of this.getAlliedField()) { + for (const pokemon of this.getAlliesGenerator()) { applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", { pokemon, tag: newTag, cancelled, target: this }); if (cancelled.value) { return false; @@ -4785,7 +4786,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - for (const pokemon of this.getAlliedField()) { + for (const pokemon of this.getAlliesGenerator()) { applyAbAttrs("UserFieldStatusEffectImmunityAbAttr", { pokemon, effect, diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index c95c349e65f..fabfad043f3 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1915,7 +1915,7 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { // Remove the Pokemon's FAINT status pokemon.resetStatus(true, false, true, false); - for (const p of pokemon.getAlliedField()) { + for (const p of pokemon.getAlliesGenerator()) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); } return true; diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 70a45ad802c..b514d726e04 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -39,7 +39,7 @@ export class PostSummonPhase extends PokemonPhase { ) { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - for (const p of pokemon.getAlliedField()) { + for (const p of pokemon.getAlliesGenerator()) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); }