[Misc] Add dynamic ordering to non-phase interactions (#6581)

* Update interactions to use generator

* Fix triggerWeatherBasedFormChanges

* Update documentation

* Fix arena tag file

* Update src/field/pokemon.ts

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

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
This commit is contained in:
Dean 2025-10-28 21:03:32 -07:00 committed by GitHub
parent b88cde162a
commit c048b34425
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 137 additions and 114 deletions

View File

@ -803,6 +803,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);

View File

@ -66,6 +66,7 @@ import type { Constructor } from "#types/common";
import type { Closed, Exact } from "#types/type-helpers";
import { coerceArray } from "#utils/array";
import { BooleanHolder, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { inSpeedOrder } from "#utils/speed-order-generator";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";
@ -2857,7 +2858,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 };
@ -3168,10 +3169,8 @@ 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 partyPokemon of allowedParty) {
for (const partyPokemon of pokemon.getAlliesGenerator()) {
if (partyPokemon.status && this.statusEffect.includes(partyPokemon.status.effect)) {
globalScene.phaseManager.queueMessage(
getStatusEffectHealText(partyPokemon.status.effect, getPokemonNameWithAffix(partyPokemon)),
@ -4423,7 +4422,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),
@ -4992,7 +4991,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;
}
@ -5545,10 +5544,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;

View File

@ -78,6 +78,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. */
@ -247,7 +248,7 @@ export abstract class ArenaTag implements BaseArenaTag {
/**
* Helper function that retrieves the Pokemon affected.
* @returns An array containing all {@linkcode Pokemon} affected by this Tag.
* @returns An array containing all {@linkcode Pokemon} affected by this Tag, not in speed order.
*/
protected getAffectedPokemon(): Pokemon[] {
switch (this.side) {
@ -1204,7 +1205,7 @@ export class GravityTag extends SerializableArenaTag {
onAdd(quiet = false): void {
super.onAdd(quiet);
globalScene.getField(true).forEach(pokemon => {
for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
if (pokemon !== null) {
pokemon.removeTag(BattlerTagType.FLOATING);
pokemon.removeTag(BattlerTagType.TELEKINESIS);
@ -1212,7 +1213,7 @@ export class GravityTag extends SerializableArenaTag {
pokemon.addTag(BattlerTagType.INTERRUPTED);
}
}
});
}
}
}
@ -1237,10 +1238,13 @@ class TailwindTag extends SerializableArenaTag {
onAdd(quiet = false): void {
super.onAdd(quiet);
const source = this.getSourcePokemon();
const field = this.getAffectedPokemon();
if (source == null) {
return;
}
for (const pokemon of field) {
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)) {
@ -1346,11 +1350,11 @@ class FireGrassPledgeTag extends SerializableArenaTag {
}
override lapse(): boolean {
const field = this.getAffectedPokemon().filter(
pokemon => !pokemon.isOfType(PokemonType.FIRE, true, true) && !pokemon.switchOutStatus,
);
for (const pokemon of inSpeedOrder(this.side)) {
if (pokemon.isOfType(PokemonType.FIRE) || pokemon.switchOutStatus) {
continue;
}
field.forEach(pokemon => {
// "{pokemonNameWithAffix} was hurt by the sea of fire!"
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:fireGrassPledgeLapse", {
@ -1365,7 +1369,7 @@ class FireGrassPledgeTag extends SerializableArenaTag {
CommonAnim.MAGMA_STORM,
);
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
});
}
return super.lapse();
}
@ -1497,7 +1501,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.id !== pokemon.id) {
// TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing
// the appropriate attributes (preLEaveField and IllusionBreak)
@ -1539,7 +1543,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
this.#beingRemoved = true;
super.onRemove(quiet);
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.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) {
[true, false].forEach(passive => {

View File

@ -98,6 +98,7 @@ import { areAllies } from "#utils/pokemon-utils";
import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { inSpeedOrder } from "#utils/speed-order-generator";
import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils";
import type { ReadonlyGenericUint8Array } from "#types/typed-arrays";
@ -1031,8 +1032,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.getAlliesGenerator()) {
applyAbAttrs("UserFieldMoveTypePowerBoostAbAttr", {pokemon: p, opponent: target, move: this, simulated, power});
}
power.value *= typeChangeMovePowerMultiplier.value;
@ -6218,8 +6220,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;
}
}
@ -8156,7 +8160,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 }));

View File

@ -40,6 +40,7 @@ import type { Constructor } from "#types/common";
import type { AbstractConstructor } from "#types/type-helpers";
import { NumberHolder, randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { inSpeedOrder } from "#utils/speed-order-generator";
export class Arena {
public biomeType: BiomeId;
@ -358,15 +359,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;
}
@ -376,11 +374,11 @@ 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) {
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;
@ -388,23 +386,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} */
@ -453,16 +451,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;
}

View File

@ -173,6 +173,7 @@ import {
import { calculateBossSegmentDamage } from "#utils/damage";
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;
@ -2468,8 +2468,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// Do not check queenly majesty unless this is being simulated
// This is because the move effect phase should not check queenly majesty, as that is handled by the move phase
if (simulated && !cancelledHolder.value) {
const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
defendingSidePlayField.forEach((p: (typeof defendingSidePlayField)[0]) => {
for (const p of this.getAlliesGenerator()) {
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
pokemon: p,
opponent: source,
@ -2477,7 +2476,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
cancelled: cancelledHolder,
simulated,
});
});
}
}
}
@ -3243,7 +3242,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)
*/
@ -3253,6 +3252,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
/**
* @returns A generator of pokemon that oppose this one in speed order
*/
public getOpponentsGenerator(): Generator<Pokemon, number> {
return inSpeedOrder(this.isPlayer() ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER);
}
getOpponentDescriptor(): string {
return this.isPlayer() ? i18next.t("arenaTag:opposingTeam") : i18next.t("arenaTag:yourTeam");
}
@ -3262,12 +3268,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 A generator of Pokémon on the allied field in speed order.
*/
getAlliedField(): Pokemon[] {
return this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
getAlliesGenerator(): Generator<Pokemon, number> {
return inSpeedOrder(this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY);
}
/**
@ -4020,16 +4024,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.getAlliesGenerator()) {
applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", {
pokemon,
tag: stubTag,
cancelled,
simulated: true,
target: this,
}),
);
});
}
return !cancelled.value;
}
@ -4063,7 +4066,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;
@ -4811,7 +4814,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,
@ -5619,10 +5622,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();

View File

@ -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.getAlliesGenerator()) {
applyAbAttrs("CommanderAbAttr", { pokemon: p });
}
return true;

View File

@ -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;
});

View File

@ -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());
}
}

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -13,6 +13,7 @@ import { getStatusEffectActivationText } from "#data/status-effect";
import { getTerrainBlockMessage } from "#data/terrain";
import { getWeatherBlockMessage } from "#data/weather";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
@ -35,6 +36,7 @@ import type { TurnMove } from "#types/turn-move";
import { applyChallenges } from "#utils/challenge-utils";
import { BooleanHolder, 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 {
@ -321,17 +323,16 @@ export class MovePhase extends PokemonPhase {
// check move redirection abilities of every pokemon *except* the user.
// TODO: Make storm drain, lightning rod, etc, redirect at this point for type changing moves
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;
@ -848,9 +849,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: user, targets });
});
}
}
}

View File

@ -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");
@ -427,7 +429,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);

View File

@ -30,8 +30,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.getAlliesGenerator()) {
applyAbAttrs("CommanderAbAttr", { pokemon: p });
}

View File

@ -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 });
}
}

View File

@ -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);

View File

@ -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";
@ -39,11 +39,16 @@ function shufflePokemonList<T extends Pokemon | hasPokemon>(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<T extends Pokemon | hasPokemon>(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;
});