diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index ea101d24896..400b3f89ef4 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8010,7 +8010,7 @@ export class MoveCondition { export class FirstMoveCondition extends MoveCondition { constructor() { - super((user, _target, _move) => user.summonData.waveTurnCount === 1); + super((user, _target, _move) => user.tempSummonData.waveTurnCount === 1); } getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number { diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index f76462d2725..506908c5fc7 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -3,7 +3,7 @@ import type Pokemon from "../field/pokemon"; import { StatusEffect } from "#enums/status-effect"; import { allMoves } from "./moves/move"; import { MoveCategory } from "#enums/MoveCategory"; -import type { Constructor, nil } from "#app/utils/common"; +import { isNullOrUndefined, type Constructor, type nil } from "#app/utils/common"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -369,12 +369,10 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge export abstract class SpeciesFormChangeMoveTrigger extends SpeciesFormChangeTrigger { public movePredicate: (m: Moves) => boolean; - public used: boolean; - constructor(move: Moves | ((m: Moves) => boolean), used = true) { + constructor(move: Moves | ((m: Moves) => boolean)) { super(); this.movePredicate = typeof move === "function" ? move : (m: Moves) => m === move; - this.used = used; } } @@ -383,7 +381,7 @@ export class SpeciesFormChangePreMoveTrigger extends SpeciesFormChangeMoveTrigge canChange(pokemon: Pokemon): boolean { const command = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; - return !!command?.move && this.movePredicate(command.move.move) === this.used; + return !isNullOrUndefined(command?.move) && this.movePredicate(command.move.move); } } @@ -391,9 +389,7 @@ export class SpeciesFormChangePostMoveTrigger extends SpeciesFormChangeMoveTrigg description = i18next.t("pokemonEvolutions:Forms.postMove"); canChange(pokemon: Pokemon): boolean { - return ( - pokemon.summonData && !!pokemon.getLastXMoves(1).filter(m => this.movePredicate(m.move)).length === this.used - ); + return this.movePredicate(pokemon.getLastXMoves()[0].move); } } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 7605f66f5be..6b77311e924 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -368,6 +368,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleData: PokemonBattleData = new PokemonBattleData(); /** Data that resets on switch or battle end (stat stages, battler tags, etc.) */ public summonData: PokemonSummonData = new PokemonSummonData(); + /** Similar to {@linkcode PokemonSummonData}, but is reset on reload (not saved to file). */ + public tempSummonData: PokemonTempSummonData = new PokemonTempSummonData(); /** Wave data correponding to moves/ability information revealed */ public waveData: PokemonWaveData = new PokemonWaveData(); /** Per-turn data like hit count & flinch tracking */ @@ -1427,7 +1429,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getStat(stat: PermanentStat, bypassSummonData = true): number { if ( !bypassSummonData && - this.summonData && this.summonData.stats[stat] !== 0 ) { return this.summonData.stats[stat]; @@ -2156,10 +2157,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - // the type added to Pokemon from moves like Forest's Curse or Trick Or Treat + // check type added to Pokemon from moves like Forest's Curse or Trick Or Treat if ( !ignoreOverride && - this.summonData && this.summonData.addedType && !types.includes(this.summonData.addedType) ) { @@ -4967,9 +4967,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } transferTagsBySourceId(sourceId: number, newSourceId: number): void { - this.summonData.tags - .filter(t => t.sourceId === sourceId) - .forEach(t => (t.sourceId = newSourceId)); + this.summonData.tags.forEach(t => { + if (t.sourceId === sourceId) { + t.sourceId = newSourceId; + } + }) } /** @@ -5646,7 +5648,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - + /** + * Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData} + * in preparation for switching pokemon, as well as removing any relevant on-switch tags. + */ resetSummonData(): void { const illusion: IllusionData | null = this.summonData.illusion; if (this.summonData.speciesForm) { @@ -5654,8 +5659,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.updateFusionPalette(); } this.summonData = new PokemonSummonData(); + this.tempSummonData = new PokemonTempSummonData(); + this.setSwitchOutStatus(false); - // TODO: Why do we remove leech seed on resetting summon data? That should be a BattlerTag thing... + // TODO: Why do we tick down leech seed on switch if (this.getTag(BattlerTagType.SEEDED)) { this.lapseTag(BattlerTagType.SEEDED); } @@ -7751,7 +7758,8 @@ export class PokemonSummonData { public tags: BattlerTag[] = []; public abilitySuppressed = false; - // Overrides for transform + // Overrides for transform. + // TODO: Move these into a separate class & add rage fist hit count public speciesForm: PokemonSpeciesForm | null = null; public fusionSpeciesForm: PokemonSpeciesForm | null = null; public ability: Abilities | undefined; @@ -7772,16 +7780,6 @@ export class PokemonSummonData { /** Array containing all berries eaten in the last turn; used by {@linkcode Abilities.CUD_CHEW} */ public berriesEatenLast: BerryType[] = []; - /** - * The number of turns this pokemon has spent in the active position since entering the battle. - * Currently exclusively used for positioning the battle cursor. - */ - public turnCount = 1; - /** - * The number of turns this pokemon has spent in the active position since starting a new wave. - * Used for most "first turn only" conditions ({@linkcode Moves.FAKE_OUT | Fake Out}, {@linkcode Moves.FIRST_IMPRESSION | First Impression}, etc.) - */ - public waveTurnCount = 1; /** * An array of all moves this pokemon has used since entering the battle. * Used for most moves and abilities that check prior move usage or copy already-used moves. @@ -7796,6 +7794,26 @@ export class PokemonSummonData { } } + // TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added +export class PokemonTempSummonData { + /** + * The number of turns this pokemon has spent without switching out. + * Only currently used for positioning the battle cursor. + */ + turnCount: number = 1; + + /** + * The number of turns this pokemon has spent in the active position since the start of the wave + * without switching out. + * Reset on switch and new wave, but not stored in `SummonData` to avoid being written to the save file. + + * Used to evaluate "first turn only" conditions such as + * {@linkcode Moves.FAKE_OUT | Fake Out} and {@linkcode Moves.FIRST_IMPRESSION | First Impression}). + */ + waveTurnCount = 1; + +} + /** * Persistent data for a {@linkcode Pokemon}. * Resets at the start of a new battle (but not on switch). @@ -7819,7 +7837,7 @@ export class PokemonBattleData { /** * Temporary data for a {@linkcode Pokemon}. - * Resets on new wave/battle start. + * Resets on new wave/battle start (but not on switch). */ export class PokemonWaveData { /** Whether the pokemon has endured due to a {@linkcode BattlerTagType.ENDURE_TOKEN} */ @@ -7866,7 +7884,7 @@ export class PokemonTurnData { public extraTurns = 0; /** * All berries eaten by this pokemon in this turn. - * Saved into {@linkcode PokemonSummonData | SummonData} by {@linkcode Abilities.CUD_CHEW) on turn end. + * Saved into {@linkcode PokemonSummonData | SummonData} by {@linkcode Abilities.CUD_CHEW} on turn end. * @see {@linkcode PokemonSummonData.berriesEatenLast} */ public berriesEaten: BerryType[] = [] diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 5ccef2c71eb..b4bb28fe55e 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -60,7 +60,7 @@ export class BattleEndPhase extends BattlePhase { for (const pokemon of globalScene.getField()) { if (pokemon) { - pokemon.summonData.waveTurnCount = 1; + pokemon.tempSummonData.waveTurnCount = 1; } } diff --git a/src/phases/field-phase.ts b/src/phases/field-phase.ts index 98c1ced510f..c37f0e960e7 100644 --- a/src/phases/field-phase.ts +++ b/src/phases/field-phase.ts @@ -6,8 +6,7 @@ type PokemonFunc = (pokemon: Pokemon) => void; export abstract class FieldPhase extends BattlePhase { executeForAll(func: PokemonFunc): void { - const field = globalScene.getField(true).filter(p => p.summonData); - for (const pokemon of field) { + for (const pokemon of globalScene.getField(true)) { func(pokemon); } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 011dd26db92..fd0c4ef7949 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -229,8 +229,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]; - const field = globalScene.getField(true).filter(p => p.summonData); - field.forEach(pokemon => { + globalScene.getField(true).forEach(pokemon => { const tags = pokemon.summonData.tags; tags .filter( diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 67691d7e225..bb31f87cc3d 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -33,10 +33,10 @@ export class SwitchSummonPhase extends SummonPhase { * @param fieldIndex - Position on the battle field * @param slotIndex - The index of pokemon (in party of 6) to switch into * @param doReturn - Whether to render "comeback" dialogue - * @param player - (Optional) `true` if the switch is from the player + * @param player - Whether the switch came from the player or enemy; default `true` */ - constructor(switchType: SwitchType, fieldIndex: number, slotIndex: number, doReturn: boolean, player?: boolean) { - super(fieldIndex, player !== undefined ? player : true); + constructor(switchType: SwitchType, fieldIndex: number, slotIndex: number, doReturn: boolean, player = true) { + super(fieldIndex, player); this.switchType = switchType; this.slotIndex = slotIndex; @@ -121,14 +121,23 @@ export class SwitchSummonPhase extends SummonPhase { switchAndSummon() { const party = this.player ? this.getParty() : globalScene.getEnemyParty(); - const switchedInPokemon = party[this.slotIndex]; + const switchedInPokemon: Pokemon | undefined = party[this.slotIndex]; this.lastPokemon = this.getPokemon(); + applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); - if (this.switchType === SwitchType.BATON_PASS && switchedInPokemon) { - (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon => + if (!switchedInPokemon) { + this.end(); + return; + } + + if (this.switchType === SwitchType.BATON_PASS) { + // If switching via baton pass, update opposing tags coming from the prior pokemon + (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id), ); + + // If the recipient pokemon lacks a baton, give our baton to it during the swap if ( !globalScene.findModifier( m => @@ -141,14 +150,8 @@ export class SwitchSummonPhase extends SummonPhase { m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id, ) as SwitchEffectTransferModifier; - if ( - batonPassModifier && - !globalScene.findModifier( - m => - m instanceof SwitchEffectTransferModifier && - (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id, - ) - ) { + + if (batonPassModifier) { globalScene.tryTransferHeldItemModifier( batonPassModifier, switchedInPokemon, @@ -161,49 +164,48 @@ export class SwitchSummonPhase extends SummonPhase { } } } - if (switchedInPokemon) { - party[this.slotIndex] = this.lastPokemon; - party[this.fieldIndex] = switchedInPokemon; - const showTextAndSummon = () => { - globalScene.ui.showText( - this.player - ? i18next.t("battle:playerGo", { - pokemonName: getPokemonNameWithAffix(switchedInPokemon), - }) - : i18next.t("battle:trainerGo", { - trainerName: globalScene.currentBattle.trainer?.getName( - !(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, - ), - pokemonName: this.getPokemon().getNameToRender(), - }), - ); - /** - * If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left. - * Otherwise, clear any persisting tags on the returned Pokemon. - */ - if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { - const substitute = this.lastPokemon.getTag(SubstituteTag); - if (substitute) { - switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; - switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; - switchedInPokemon.setAlpha(0.5); - } - } else { - switchedInPokemon.resetSummonData(); + + party[this.slotIndex] = this.lastPokemon; + party[this.fieldIndex] = switchedInPokemon; + const showTextAndSummon = () => { + globalScene.ui.showText( + this.player + ? i18next.t("battle:playerGo", { + pokemonName: getPokemonNameWithAffix(switchedInPokemon), + }) + : i18next.t("battle:trainerGo", { + trainerName: globalScene.currentBattle.trainer?.getName( + !(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER, + ), + pokemonName: this.getPokemon().getNameToRender(), + }), + ); + + /** + * If this switch is passing a Substitute, make the switched Pokemon matches the returned Pokemon's state as it left. + * Otherwise, clear any persisting tags on the returned Pokemon. + */ + if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { + const substitute = this.lastPokemon.getTag(SubstituteTag); + if (substitute) { + switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; + switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; + switchedInPokemon.setAlpha(0.5); } - this.summon(); - }; - if (this.player) { - showTextAndSummon(); } else { - globalScene.time.delayedCall(1500, () => { - this.hideEnemyTrainer(); - globalScene.pbTrayEnemy.hide(); - showTextAndSummon(); - }); + switchedInPokemon.resetSummonData(); } + this.summon(); + }; + + if (this.player) { + showTextAndSummon(); } else { - this.end(); + globalScene.time.delayedCall(1500, () => { + this.hideEnemyTrainer(); + globalScene.pbTrayEnemy.hide(); + showTextAndSummon(); + }); } } @@ -221,15 +223,15 @@ export class SwitchSummonPhase extends SummonPhase { const lastPokemonHasForceSwitchAbAttr = this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted(); - // Compensate for turn spent summoning - // Or compensate for force switch move if switched out pokemon is not fainted + // Compensate for turn spent summoning/forced switch if switched out pokemon is not fainted. + // Needed as we increment turn counters in `TurnEndPhase`. if ( currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted || lastPokemonHasForceSwitchAbAttr ) { - pokemon.summonData.turnCount--; - pokemon.summonData.waveTurnCount--; + pokemon.tempSummonData.turnCount--; + pokemon.tempSummonData.waveTurnCount--; } if (this.switchType === SwitchType.BATON_PASS && pokemon) { @@ -247,7 +249,7 @@ export class SwitchSummonPhase extends SummonPhase { pokemon.turnData.switchedInThisTurn = true; } - this.lastPokemon?.resetSummonData(); + this.lastPokemon.resetSummonData(); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index c7868324209..756c497802b 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -56,8 +56,8 @@ export class TurnEndPhase extends FieldPhase { globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); - pokemon.summonData.turnCount++; - pokemon.summonData.waveTurnCount++; + pokemon.tempSummonData.turnCount++; + pokemon.tempSummonData.waveTurnCount++; }; this.executeForAll(handlePokemon); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 622b9cdcbd1..b802780bbb8 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -72,19 +72,16 @@ export class TurnStartPhase extends FieldPhase { // This occurs before the main loop because of battles with more than two Pokemon const battlerBypassSpeed = {}; - globalScene - .getField(true) - .filter(p => p.summonData) - .map(p => { - const bypassSpeed = new BooleanHolder(false); - const canCheckHeldItems = new BooleanHolder(true); - applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed); - applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems); - if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); - } - battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; - }); + globalScene.getField(true).map(p => { + const bypassSpeed = new BooleanHolder(false); + const canCheckHeldItems = new BooleanHolder(true); + applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed); + applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems); + if (canCheckHeldItems.value) { + globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); + } + battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; + }); // The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands. diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index e96bf8b8cf2..cabe897d7b6 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -794,7 +794,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.BATTLE_INFO); nameTextWidth = nameSizeTest.displayWidth; - const gender = pokemon.summonData?.illusion?.gender ?? pokemon.gender; + const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender; while ( nameTextWidth > (this.player || !this.boss ? 60 : 98) - diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index fc194bff962..5a0978a934d 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -127,7 +127,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle { messageHandler.commandWindow.setVisible(false); messageHandler.movesWindowContainer.setVisible(true); const pokemon = (globalScene.getCurrentPhase() as CommandPhase).getPokemon(); - if (pokemon.summonData.turnCount <= 1) { + if (pokemon.tempSummonData.turnCount <= 1) { this.setCursor(0); } else { this.setCursor(this.getCursor()); diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 914914b2a4e..5e14e5f7771 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -71,7 +71,7 @@ export default class TargetSelectUiHandler extends UiHandler { */ resetCursor(cursorN: number, user: Pokemon): void { if (!isNullOrUndefined(cursorN)) { - if ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.summonData.waveTurnCount === 1) { + if ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.tempSummonData.waveTurnCount === 1) { // Reset cursor on the first turn of a fight or if an ally was targeted last turn cursorN = -1; }