diff --git a/public/audio/bgm/graveyard.mp3 b/public/audio/bgm/graveyard.mp3 index 48092fa3ec2..45571f16d09 100644 Binary files a/public/audio/bgm/graveyard.mp3 and b/public/audio/bgm/graveyard.mp3 differ diff --git a/public/images/events/halloween2024-event-es.png b/public/images/events/halloween2024-event-es-ES.png similarity index 100% rename from public/images/events/halloween2024-event-es.png rename to public/images/events/halloween2024-event-es-ES.png diff --git a/public/images/statuses_es.json b/public/images/statuses_es-ES.json similarity index 98% rename from public/images/statuses_es.json rename to public/images/statuses_es-ES.json index 4b44aa117e4..dbb3783842a 100644 --- a/public/images/statuses_es.json +++ b/public/images/statuses_es-ES.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "statuses_es.png", + "image": "statuses_es-ES.png", "format": "RGBA8888", "size": { "w": 22, diff --git a/public/images/statuses_es.png b/public/images/statuses_es-ES.png similarity index 100% rename from public/images/statuses_es.png rename to public/images/statuses_es-ES.png diff --git a/public/images/types_es.json b/public/images/types_es-ES.json similarity index 99% rename from public/images/types_es.json rename to public/images/types_es-ES.json index 0fb922e8939..198899c0f12 100644 --- a/public/images/types_es.json +++ b/public/images/types_es-ES.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "types_es.png", + "image": "types_es-ES.png", "format": "RGBA8888", "size": { "w": 32, diff --git a/public/images/types_es.png b/public/images/types_es-ES.png similarity index 100% rename from public/images/types_es.png rename to public/images/types_es-ES.png diff --git a/public/locales b/public/locales index 71390cba88f..fc4a1effd51 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f +Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 3cbf4d7b422..415a8fe9aaf 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -15,7 +15,7 @@ import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "#app/data/move"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import AbilityBar from "#app/ui/ability-bar"; -import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "#app/data/ability"; +import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr, PostItemLostAbAttr } from "#app/data/ability"; import Battle, { BattleType, FixedBattleConfig } from "#app/battle"; import { GameMode, GameModes, getGameMode } from "#app/game-mode"; import FieldSpritePipeline from "#app/pipelines/field-sprite"; @@ -323,6 +323,7 @@ export default class BattleScene extends SceneBase { this.conditionalQueue = []; this.phaseQueuePrependSpliceIndex = -1; this.nextCommandPhaseQueue = []; + this.eventManager = new TimedEventManager(); this.updateGameInfo(); } @@ -378,7 +379,6 @@ export default class BattleScene extends SceneBase { this.fieldSpritePipeline = new FieldSpritePipeline(this.game); (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline); - this.eventManager = new TimedEventManager(); this.launchBattle(); } @@ -764,57 +764,65 @@ export default class BattleScene extends SceneBase { return true; } - getParty(): PlayerPokemon[] { + public getPlayerParty(): PlayerPokemon[] { return this.party; } - getPlayerPokemon(): PlayerPokemon | undefined { - return this.getPlayerField().find(p => p.isActive()); - } - /** - * Finds the first {@linkcode Pokemon.isActive() | active PlayerPokemon} that isn't also currently switching out - * @returns Either the first {@linkcode PlayerPokemon} satisfying, or undefined if no player pokemon on the field satisfy + * @returns An array of {@linkcode PlayerPokemon} filtered from the player's party + * that are {@linkcode PlayerPokemon.isAllowedInBattle | allowed in battle}. */ - getNonSwitchedPlayerPokemon(): PlayerPokemon | undefined { - return this.getPlayerField().find(p => p.isActive() && p.switchOutStatus === false); + public getPokemonAllowedInBattle(): PlayerPokemon[] { + return this.getPlayerParty().filter(p => p.isAllowedInBattle()); } /** - * Returns an array of PlayerPokemon of length 1 or 2 depending on if double battles or not + * @returns The first {@linkcode PlayerPokemon} that is {@linkcode getPlayerField on the field} + * and {@linkcode PlayerPokemon.isActive is active} + * (aka {@linkcode PlayerPokemon.isAllowedInBattle is allowed in battle}), + * or `undefined` if there are no valid pokemon + * @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true` + */ + public getPlayerPokemon(includeSwitching: boolean = true): PlayerPokemon | undefined { + return this.getPlayerField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false)); + } + + /** + * Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not. + * Does not actually check if the pokemon are on the field or not. * @returns array of {@linkcode PlayerPokemon} */ - getPlayerField(): PlayerPokemon[] { - const party = this.getParty(); + public getPlayerField(): PlayerPokemon[] { + const party = this.getPlayerParty(); return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); } - getEnemyParty(): EnemyPokemon[] { + public getEnemyParty(): EnemyPokemon[] { return this.currentBattle?.enemyParty ?? []; } - getEnemyPokemon(): EnemyPokemon | undefined { - return this.getEnemyField().find(p => p.isActive()); - } - /** - * Finds the first {@linkcode Pokemon.isActive() | active EnemyPokemon} pokemon from the enemy that isn't also currently switching out - * @returns Either the first {@linkcode EnemyPokemon} satisfying, or undefined if no player pokemon on the field satisfy + * @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField on the field} + * and {@linkcode EnemyPokemon.isActive is active} + * (aka {@linkcode EnemyPokemon.isAllowedInBattle is allowed in battle}), + * or `undefined` if there are no valid pokemon + * @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true` */ - getNonSwitchedEnemyPokemon(): EnemyPokemon | undefined { - return this.getEnemyField().find(p => p.isActive() && p.switchOutStatus === false); + public getEnemyPokemon(includeSwitching: boolean = true): EnemyPokemon | undefined { + return this.getEnemyField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false)); } /** - * Returns an array of EnemyPokemon of length 1 or 2 depending on if double battles or not + * Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not. + * Does not actually check if the pokemon are on the field or not. * @returns array of {@linkcode EnemyPokemon} */ - getEnemyField(): EnemyPokemon[] { + public getEnemyField(): EnemyPokemon[] { const party = this.getEnemyParty(); return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); } - getField(activeOnly: boolean = false): Pokemon[] { + public getField(activeOnly: boolean = false): Pokemon[] { const ret = new Array(4).fill(null); const playerField = this.getPlayerField(); const enemyField = this.getEnemyField(); @@ -867,7 +875,7 @@ export default class BattleScene extends SceneBase { getPokemonById(pokemonId: integer): Pokemon | null { const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); - return (findInParty(this.getParty()) || findInParty(this.getEnemyParty())) ?? null; + return (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null; } addPlayerPokemon(species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void): PlayerPokemon { @@ -1062,7 +1070,7 @@ export default class BattleScene extends SceneBase { this.modifierBar.removeAll(true); this.enemyModifierBar.removeAll(true); - for (const p of this.getParty()) { + for (const p of this.getPlayerParty()) { p.destroy(); } this.party = []; @@ -1275,7 +1283,7 @@ export default class BattleScene extends SceneBase { } }); - for (const pokemon of this.getParty()) { + for (const pokemon of this.getPlayerParty()) { pokemon.resetBattleData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } @@ -1285,7 +1293,7 @@ export default class BattleScene extends SceneBase { } } - for (const pokemon of this.getParty()) { + for (const pokemon of this.getPlayerParty()) { this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger); } @@ -1480,7 +1488,7 @@ export default class BattleScene extends SceneBase { } trySpreadPokerus(): void { - const party = this.getParty(); + const party = this.getPlayerParty(); const infectedIndexes: integer[] = []; const spread = (index: number, spreadTo: number) => { const partyMember = party[index + spreadTo]; @@ -1677,7 +1685,7 @@ export default class BattleScene extends SceneBase { updateAndShowText(duration: number): void { const labels = [ this.luckLabelText, this.luckText ]; labels.forEach(t => t.setAlpha(0)); - const luckValue = getPartyLuckValue(this.getParty()); + const luckValue = getPartyLuckValue(this.getPlayerParty()); this.luckText.setText(getLuckString(luckValue)); if (luckValue < 14) { this.luckText.setTint(getLuckTextTint(luckValue)); @@ -2593,9 +2601,19 @@ export default class BattleScene extends SceneBase { const addModifier = () => { if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { if (target.isPlayer()) { - this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => resolve(true)); + this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => { + if (source) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + } + resolve(true); + }); } else { - this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => resolve(true)); + this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => { + if (source) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + } + resolve(true); + }); } } else { resolve(false); @@ -2615,7 +2633,7 @@ export default class BattleScene extends SceneBase { removePartyMemberModifiers(partyMemberIndex: integer): Promise { return new Promise(resolve => { - const pokemonId = this.getParty()[partyMemberIndex].id; + const pokemonId = this.getPlayerParty()[partyMemberIndex].id; const modifiersToRemove = this.modifiers.filter(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId); for (const m of modifiersToRemove) { this.modifiers.splice(this.modifiers.indexOf(m), 1); @@ -2742,7 +2760,7 @@ export default class BattleScene extends SceneBase { } } - this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyParty(), instant).then(() => { + this.updatePartyForModifiers(player ? this.getPlayerParty() : this.getEnemyParty(), instant).then(() => { (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); if (!player) { this.updateUIPositions(); @@ -2980,7 +2998,7 @@ export default class BattleScene extends SceneBase { */ getActiveKeys(): string[] { const keys: string[] = []; - const playerParty = this.getParty(); + const playerParty = this.getPlayerParty(); playerParty.forEach(p => { keys.push(p.getSpriteKey(true)); keys.push(p.getBattleSpriteKey(true, true)); @@ -3016,7 +3034,7 @@ export default class BattleScene extends SceneBase { this.setFieldScale(0.75); this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); this.currentBattle.double = true; - const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle()); + const availablePartyMembers = this.getPlayerParty().filter((p) => p.isAllowedInBattle()); if (availablePartyMembers.length > 1) { this.pushPhase(new ToggleDoublePositionPhase(this, true)); if (!availablePartyMembers[1].isOnField()) { @@ -3041,7 +3059,7 @@ export default class BattleScene extends SceneBase { */ applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set): void { const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; - const party = this.getParty(); + const party = this.getPlayerParty(); const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; diff --git a/src/data/ability.ts b/src/data/ability.ts index 58824603bc3..8eeb5af52b8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,4 +1,4 @@ -import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; +import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; @@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; -import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; +import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import i18next from "i18next"; @@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales"; import { Command } from "../ui/command-ui-handler"; import { BerryModifierType } from "#app/modifier/modifier-type"; import { getPokeballName } from "./pokeball"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex, BattleType } from "#app/battle"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import BattleScene from "#app/battle-scene"; +import { SwitchType } from "#app/enums/switch-type"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; export class Ability implements Localizable { public id: Abilities; @@ -1950,6 +1956,10 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { } } +/** + * Ability attribute for ignoring the opponent's stat changes + * @param stats the stats that should be ignored + */ export class IgnoreOpponentStatStagesAbAttr extends AbAttr { private stats: readonly BattleStat[]; @@ -1959,6 +1969,15 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr { this.stats = stats ?? BATTLE_STATS; } + /** + * Modifies a BooleanHolder and returns the result to see if a stat is ignored or not + * @param _pokemon n/a + * @param _passive n/a + * @param simulated n/a + * @param _cancelled n/a + * @param args A BooleanHolder that represents whether or not to ignore a stat's stat changes + * @returns true if the stat is ignored, false otherwise + */ apply(_pokemon: Pokemon, _passive: boolean, simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]) { if (this.stats.includes(args[0])) { (args[1] as Utils.BooleanHolder).value = true; @@ -3092,7 +3111,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr { /** * Condition function to applied to abilities related to Sheer Force. * Checks if last move used against target was affected by a Sheer Force user and: - * Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell + * Disables: Color Change, Pickpocket, Berserk, Anger Shell * @returns {AbAttrCondition} If false disables the ability which the condition is applied to. */ function getSheerForceHitDisableAbCondition(): AbAttrCondition { @@ -3614,22 +3633,19 @@ export class MoodyAbAttr extends PostTurnAbAttr { } } -export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr { - private stats: BattleStat[]; - private stages: number; +export class SpeedBoostAbAttr extends PostTurnAbAttr { - constructor(stats: BattleStat[], stages: number) { + constructor() { super(true); - - this.stats = Array.isArray(stats) - ? stats - : [ stats ]; - this.stages = stages; } applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { if (!simulated) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + if (!pokemon.turnData.switchedInThisTurn && !pokemon.turnData.failedRunAway) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPD ], 1)); + } else { + return false; + } } return true; } @@ -3841,6 +3857,41 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } +/** + * Triggers after the Pokemon loses or consumes an item + * @extends AbAttr + */ +export class PostItemLostAbAttr extends AbAttr { + applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise { + return false; + } +} + +/** + * Applies a Battler Tag to the Pokemon after it loses or consumes item + * @extends PostItemLostAbAttr + */ +export class PostItemLostApplyBattlerTagAbAttr extends PostItemLostAbAttr { + private tagType: BattlerTagType; + constructor(tagType: BattlerTagType) { + super(true); + this.tagType = tagType; + } + /** + * Adds the last used Pokeball back into the player's inventory + * @param pokemon {@linkcode Pokemon} with this ability + * @param args N/A + * @returns true if BattlerTag was applied + */ + applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise { + if (!pokemon.getTag(this.tagType) && !simulated) { + pokemon.addTag(this.tagType); + return true; + } + return false; + } +} + export class StatStageChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; @@ -4703,6 +4754,84 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { } } +/** + * This applies a terrain-based type change to the Pokemon. + * Used by Mimicry. + */ +export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr { + constructor() { + super(true); + } + + override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, _args: any[]): boolean { + if (pokemon.isTerastallized()) { + return false; + } + const currentTerrain = pokemon.scene.arena.getTerrainType(); + const typeChange: Type[] = this.determineTypeChange(pokemon, currentTerrain); + if (typeChange.length !== 0) { + if (pokemon.summonData.addedType && typeChange.includes(pokemon.summonData.addedType)) { + pokemon.summonData.addedType = null; + } + pokemon.summonData.types = typeChange; + pokemon.updateInfo(); + } + return true; + } + + /** + * Retrieves the type(s) the Pokemon should change to in response to a terrain + * @param pokemon + * @param currentTerrain {@linkcode TerrainType} + * @returns a list of type(s) + */ + private determineTypeChange(pokemon: Pokemon, currentTerrain: TerrainType): Type[] { + const typeChange: Type[] = []; + switch (currentTerrain) { + case TerrainType.ELECTRIC: + typeChange.push(Type.ELECTRIC); + break; + case TerrainType.MISTY: + typeChange.push(Type.FAIRY); + break; + case TerrainType.GRASSY: + typeChange.push(Type.GRASS); + break; + case TerrainType.PSYCHIC: + typeChange.push(Type.PSYCHIC); + break; + default: + pokemon.getTypes(false, false, true).forEach(t => { + typeChange.push(t); + }); + break; + } + return typeChange; + } + + /** + * Checks if the Pokemon should change types if summoned into an active terrain + * @returns `true` if there is an active terrain requiring a type change | `false` if not + */ + override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { + if (pokemon.scene.arena.getTerrainType() !== TerrainType.NONE) { + return this.apply(pokemon, passive, simulated, new Utils.BooleanHolder(false), []); + } + return false; + } + + override getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]) { + const currentTerrain = pokemon.scene.arena.getTerrainType(); + const pokemonNameWithAffix = getPokemonNameWithAffix(pokemon); + if (currentTerrain === TerrainType.NONE) { + return i18next.t("abilityTriggers:pokemonTypeChangeRevert", { pokemonNameWithAffix }); + } else { + const moveType = i18next.t(`pokemonInfo:Type.${Type[this.determineTypeChange(pokemon, currentTerrain)[0]]}`); + return i18next.t("abilityTriggers:pokemonTypeChange", { pokemonNameWithAffix, moveType }); + } + } +} + async function applyAbAttrsInternal( attrType: Constructor, pokemon: Pokemon | null, @@ -4758,6 +4887,239 @@ async function applyAbAttrsInternal( } } +class ForceSwitchOutHelper { + constructor(private switchType: SwitchType) {} + + /** + * Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type. + * + * @param pokemon The {@linkcode Pokemon} attempting to switch out. + * @returns `true` if the switch is successful + */ + public switchOutLogic(pokemon: Pokemon): boolean { + const switchOutTarget = pokemon; + /** + * If the switch-out target is a player-controlled Pokémon, the function checks: + * - Whether there are available party members to switch in. + * - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated. + */ + if (switchOutTarget instanceof PlayerPokemon) { + if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } + + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); + return true; + } + /** + * For non-wild battles, it checks if the opposing party has any available Pokémon to switch in. + * If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated. + */ + } else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) { + if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), + (pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), + false, false), MoveEndPhase); + return true; + } + /** + * For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles). + */ + } else { + if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + return false; + } + + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(false); + pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); + + if (switchOutTarget.scene.currentBattle.double) { + const allyPokemon = switchOutTarget.getAlly(); + switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); + } + } + + if (!switchOutTarget.getAlly()?.isActive(true)) { + pokemon.scene.clearEnemyHeldItemModifiers(); + + if (switchOutTarget.hp) { + pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene)); + pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene)); + } + } + } + return false; + } + + /** + * Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions. + * + * @param pokemon The Pokémon attempting to switch out. + * @param opponent The opponent Pokémon. + * @returns `true` if the switch-out condition is met + */ + public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean { + const switchOutTarget = pokemon; + const player = switchOutTarget instanceof PlayerPokemon; + + if (player) { + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility); + return !blockedByAbility.value; + } + + if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) { + if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + return false; + } + } + + if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) { + return false; + } + + const party = player ? pokemon.scene.getPlayerParty() : pokemon.scene.getEnemyParty(); + return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) + || party.filter(p => p.isAllowedInBattle() + && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount(); + } + + /** + * Returns a message if the switch-out attempt fails due to ability effects. + * + * @param target The target Pokémon. + * @returns The failure message, or `null` if no failure. + */ + public getFailedText(target: Pokemon): string | null { + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null; + } +} + +/** + * Calculates the amount of recovery from the Shell Bell item. + * + * If the Pokémon is holding a Shell Bell, this function computes the amount of health + * recovered based on the damage dealt in the current turn. The recovery is multiplied by the + * Shell Bell's modifier (if any). + * + * @param pokemon - The Pokémon whose Shell Bell recovery is being calculated. + * @returns The amount of health recovered by Shell Bell. + */ +function calculateShellBellRecovery(pokemon: Pokemon): number { + const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); + if (shellBellModifier) { + return Utils.toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; + } + return 0; +} + +/** + * Triggers after the Pokemon takes any damage + * @extends AbAttr + */ +export class PostDamageAbAttr extends AbAttr { + public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise { + return false; + } +} + +/** + * Ability attribute for forcing a Pokémon to switch out after its health drops below half. + * This attribute checks various conditions related to the damage received, the moves used by the Pokémon + * and its opponents, and determines whether a forced switch-out should occur. + * + * Used by Wimp Out and Emergency Exit + * + * @extends PostDamageAbAttr + * @see {@linkcode applyPostDamage} + */ +export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr { + private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH); + private hpRatio: number; + + constructor(hpRatio: number = 0.5) { + super(); + this.hpRatio = hpRatio; + } + + /** + * Applies the switch-out logic after the Pokémon takes damage. + * Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and + * the Pokémon's health after damage to determine whether the switch-out should occur. + * + * @param pokemon The Pokémon that took damage. + * @param damage The amount of damage taken by the Pokémon. + * @param passive N/A + * @param simulated Whether the ability is being simulated. + * @param args N/A + * @param source The Pokemon that dealt damage + * @returns `true` if the switch-out logic was successfully applied + */ + public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise { + const moveHistory = pokemon.getMoveHistory(); + // Will not activate when the Pokémon's HP is lowered by cutting its own HP + const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]; + if (moveHistory.length > 0) { + const lastMoveUsed = moveHistory[moveHistory.length - 1]; + if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) { + return false; + } + } + + // Dragon Tail and Circle Throw switch out Pokémon before the Ability activates. + const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ]; + if (source) { + const enemyMoveHistory = source.getMoveHistory(); + if (enemyMoveHistory.length > 0) { + const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1]; + // Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop. + if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) { + return false; + // Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force. + } else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) { + return false; + // Activate only after the last hit of multistrike moves + } else if (source.turnData.hitsLeft > 1) { + return false; + } + if (source.turnData.hitCount > 1) { + damage = pokemon.turnData.damageTaken; + } + } + } + + if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) { + // Activates if it falls below half and recovers back above half from a Shell Bell + const shellBellHeal = calculateShellBellRecovery(pokemon); + if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) { + for (const opponent of pokemon.getOpponents()) { + if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { + return false; + } + } + return this.helper.switchOutLogic(pokemon); + } else { + return false; + } + } else { + return false; + } + } + public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + return this.helper.getFailedText(target); + } +} + + export function applyAbAttrs(attrType: Constructor, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated); } @@ -4791,6 +5153,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); } +export function applyPostDamageAbAttrs(attrType: Constructor, + pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args); +} + /** * Applies a field Stat multiplier attribute * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being @@ -4896,6 +5263,11 @@ export function applyPostFaintAbAttrs(attrType: Constructor, return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), args, false, simulated); } +export function applyPostItemLostAbAttrs(attrType: Constructor, + pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args); +} + function queueShowAbility(pokemon: Pokemon, passive: boolean): void { pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, passive)); pokemon.scene.clearPhaseQueueSplice(); @@ -4933,7 +5305,7 @@ export function initAbilities() { .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), new Ability(Abilities.SPEED_BOOST, 3) - .attr(PostTurnStatStageChangeAbAttr, [ Stat.SPD ], 1), + .attr(SpeedBoostAbAttr), new Ability(Abilities.BATTLE_ARMOR, 3) .attr(BlockCritAbAttr) .ignorable(), @@ -4987,7 +5359,8 @@ export function initAbilities() { .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .ignorable(), new Ability(Abilities.SHIELD_DUST, 3) - .attr(IgnoreMoveEffectsAbAttr), + .attr(IgnoreMoveEffectsAbAttr) + .ignorable(), new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) @@ -5041,6 +5414,7 @@ export function initAbilities() { new Ability(Abilities.ILLUMINATE, 3) .attr(ProtectStatAbAttr, Stat.ACC) .attr(DoubleBattleChanceAbAttr) + .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ]) .ignorable(), new Ability(Abilities.TRACE, 3) .attr(PostSummonCopyAbilityAbAttr) @@ -5105,11 +5479,9 @@ export function initAbilities() { new Ability(Abilities.CUTE_CHARM, 3) .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), new Ability(Abilities.PLUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) - .ignorable(), + .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(Abilities.MINUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) - .ignorable(), + .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(Abilities.FORECAST, 3) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) @@ -5189,7 +5561,7 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .unimplemented(), + .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) @@ -5262,7 +5634,7 @@ export function initAbilities() { new Ability(Abilities.FOREWARN, 4) .attr(ForewarnAbAttr), new Ability(Abilities.UNAWARE, 4) - .attr(IgnoreOpponentStatStagesAbAttr) + .attr(IgnoreOpponentStatStagesAbAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.ACC, Stat.EVA ]) .ignorable(), new Ability(Abilities.TINTED_LENS, 4) .attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5), @@ -5447,7 +5819,8 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), new Ability(Abilities.AROMA_VEIL, 6) - .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]), + .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]) + .ignorable(), new Ability(Abilities.FLOWER_VEIL, 6) .ignorable() .unimplemented(), @@ -5532,11 +5905,11 @@ export function initAbilities() { new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) - .condition(getSheerForceHitDisableAbCondition()) - .unimplemented(), + .attr(PostDamageForceSwitchAbAttr) + .edgeCase(), // Should not trigger when hurting itself in confusion new Ability(Abilities.EMERGENCY_EXIT, 7) - .condition(getSheerForceHitDisableAbCondition()) - .unimplemented(), + .attr(PostDamageForceSwitchAbAttr) + .edgeCase(), // Should not trigger when hurting itself in confusion new Ability(Abilities.WATER_COMPACTION, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), new Ability(Abilities.MERCILESS, 7) @@ -5767,7 +6140,7 @@ export function initAbilities() { new Ability(Abilities.POWER_SPOT, 8) .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3), new Ability(Abilities.MIMICRY, 8) - .unimplemented(), + .attr(TerrainEventTypeChangeAbAttr), new Ability(Abilities.SCREEN_CLEANER, 8) .attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]), new Ability(Abilities.STEELY_SPIRIT, 8) @@ -5898,16 +6271,14 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.SWORD_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) - .ignorable(), + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })), new Ability(Abilities.TABLETS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .ignorable(), new Ability(Abilities.BEADS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) - .ignorable(), + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })), new Ability(Abilities.ORICHALCUM_PULSE, 9) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) @@ -5924,7 +6295,7 @@ export function initAbilities() { .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(Abilities.SUPREME_OVERLORD, 9) .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5)) - .partial(), // Counter resets every wave + .partial(), // Counter resets every wave instead of on arena reset new Ability(Abilities.COSTAR, 9) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(Abilities.TOXIC_DEBRIS, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 43de6e02dcb..fce46f0c98d 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1203,6 +1203,24 @@ class GrassWaterPledgeTag extends ArenaTag { } } +/** + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Fairy_Lock_(move) Fairy Lock}. + * Fairy Lock prevents all Pokémon (except Ghost types) on the field from switching out or + * fleeing during their next turn. + * If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn, + * the Pokémon that replaces it will still be unable to switch out in the following turn. + */ +export class FairyLockTag extends ArenaTag { + constructor(turnCount: number, sourceId: number) { + super(ArenaTagType.FAIRY_LOCK, turnCount, Moves.FAIRY_LOCK, sourceId); + } + + onAdd(arena: Arena): void { + arena.scene.queueMessage(i18next.t("arenaTag:fairyLockOnAdd")); + } + +} + // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { @@ -1261,6 +1279,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove return new WaterFirePledgeTag(sourceId, side); case ArenaTagType.GRASS_WATER_PLEDGE: return new GrassWaterPledgeTag(sourceId, side); + case ArenaTagType.FAIRY_LOCK: + return new FairyLockTag(turnCount, sourceId); default: return null; } diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 4a6e44e0d51..8f22b288f45 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -478,7 +478,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.NINCADA]: [ new SpeciesEvolution(Species.NINJASK, 20, null, null), - new SpeciesEvolution(Species.SHEDINJA, 20, null, new SpeciesEvolutionCondition(p => p.scene.getParty().length < 6 && p.scene.pokeballCounts[PokeballType.POKEBALL] > 0)) + new SpeciesEvolution(Species.SHEDINJA, 20, null, new SpeciesEvolutionCondition(p => p.scene.getPlayerParty().length < 6 && p.scene.pokeballCounts[PokeballType.POKEBALL] > 0)) ], [Species.WHISMUR]: [ new SpeciesEvolution(Species.LOUDRED, 20, null, null) @@ -890,7 +890,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GOGOAT, 32, null, null) ], [Species.PANCHAM]: [ - new SpeciesEvolution(Species.PANGORO, 32, null, new SpeciesEvolutionCondition(p => !!p.scene.getParty().find(p => p.getTypes(false, false, true).indexOf(Type.DARK) > -1)), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.PANGORO, 32, null, new SpeciesEvolutionCondition(p => !!p.scene.getPlayerParty().find(p => p.getTypes(false, false, true).indexOf(Type.DARK) > -1)), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.ESPURR]: [ new SpeciesFormEvolution(Species.MEOWSTIC, "", "female", 25, null, new SpeciesEvolutionCondition(p => p.gender === Gender.FEMALE, p => p.gender = Gender.FEMALE)), @@ -1443,7 +1443,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.ROCKRUFF]: [ new SpeciesFormEvolution(Species.LYCANROC, "", "midday", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY) && (p.formIndex === 0))), - new SpeciesFormEvolution(Species.LYCANROC, "", "dusk", 25, null, new SpeciesEvolutionCondition(p => p.formIndex === 1)), + new SpeciesFormEvolution(Species.LYCANROC, "own-tempo", "dusk", 25, null, new SpeciesEvolutionCondition(p => p.formIndex === 1)), new SpeciesFormEvolution(Species.LYCANROC, "", "midnight", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT) && (p.formIndex === 0))) ], [Species.STEENEE]: [ diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index 1a509637e05..7b65ae65ec4 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -12318,6 +12318,7 @@ export const tmSpecies: TmSpecies = { Species.TURTWIG, Species.GROTLE, Species.TORTERRA, + Species.BASTIODON, Species.CHINGLING, Species.BRONZOR, Species.BRONZONG, @@ -12325,6 +12326,7 @@ export const tmSpecies: TmSpecies = { Species.WEAVILE, Species.MAGNEZONE, Species.TANGROWTH, + Species.ELECTIVIRE, Species.TOGEKISS, Species.MAMOSWINE, Species.GALLADE, @@ -12357,6 +12359,8 @@ export const tmSpecies: TmSpecies = { Species.CRYOGONAL, Species.MIENFOO, Species.MIENSHAO, + Species.GOLETT, + Species.GOLURK, Species.HYDREIGON, Species.COBALION, Species.TERRAKION, @@ -40233,6 +40237,1065 @@ export const tmSpecies: TmSpecies = { Species.HISUI_ZORUA, Species.HISUI_ZOROARK, ], + [Moves.SECRET_POWER]: [ + Species.BULBASAUR, + Species.IVYSAUR, + Species.VENUSAUR, + Species.CHARMANDER, + Species.CHARMELEON, + Species.CHARIZARD, + Species.SQUIRTLE, + Species.WARTORTLE, + Species.BLASTOISE, + Species.BUTTERFREE, + Species.BEEDRILL, + Species.PIDGEY, + Species.PIDGEOTTO, + Species.PIDGEOT, + Species.RATTATA, + Species.RATICATE, + Species.SPEAROW, + Species.FEAROW, + Species.EKANS, + Species.ARBOK, + Species.PIKACHU, + Species.RAICHU, + Species.SANDSHREW, + Species.SANDSLASH, + Species.NIDORAN_F, + Species.NIDORINA, + Species.NIDOQUEEN, + Species.NIDORAN_M, + Species.NIDORINO, + Species.NIDOKING, + Species.CLEFAIRY, + Species.CLEFABLE, + Species.VULPIX, + Species.NINETALES, + Species.JIGGLYPUFF, + Species.WIGGLYTUFF, + Species.ZUBAT, + Species.GOLBAT, + Species.ODDISH, + Species.GLOOM, + Species.VILEPLUME, + Species.PARAS, + Species.PARASECT, + Species.VENONAT, + Species.VENOMOTH, + Species.DIGLETT, + Species.DUGTRIO, + Species.MEOWTH, + Species.PERSIAN, + Species.PSYDUCK, + Species.GOLDUCK, + Species.MANKEY, + Species.PRIMEAPE, + Species.GROWLITHE, + Species.ARCANINE, + Species.POLIWAG, + Species.POLIWHIRL, + Species.POLIWRATH, + Species.ABRA, + Species.KADABRA, + Species.ALAKAZAM, + Species.MACHOP, + Species.MACHOKE, + Species.MACHAMP, + Species.BELLSPROUT, + Species.WEEPINBELL, + Species.VICTREEBEL, + Species.TENTACOOL, + Species.TENTACRUEL, + Species.GEODUDE, + Species.GRAVELER, + Species.GOLEM, + Species.PONYTA, + Species.RAPIDASH, + Species.SLOWPOKE, + Species.SLOWBRO, + Species.MAGNEMITE, + Species.MAGNETON, + Species.FARFETCHD, + Species.DODUO, + Species.DODRIO, + Species.SEEL, + Species.DEWGONG, + Species.GRIMER, + Species.MUK, + Species.SHELLDER, + Species.CLOYSTER, + Species.GASTLY, + Species.HAUNTER, + Species.GENGAR, + Species.ONIX, + Species.DROWZEE, + Species.HYPNO, + Species.KRABBY, + Species.KINGLER, + Species.VOLTORB, + Species.ELECTRODE, + Species.EXEGGCUTE, + Species.EXEGGUTOR, + Species.CUBONE, + Species.MAROWAK, + Species.HITMONLEE, + Species.HITMONCHAN, + Species.LICKITUNG, + Species.KOFFING, + Species.WEEZING, + Species.RHYHORN, + Species.RHYDON, + Species.CHANSEY, + Species.TANGELA, + Species.KANGASKHAN, + Species.HORSEA, + Species.SEADRA, + Species.GOLDEEN, + Species.SEAKING, + Species.STARYU, + Species.STARMIE, + Species.MR_MIME, + Species.SCYTHER, + Species.JYNX, + Species.ELECTABUZZ, + Species.MAGMAR, + Species.PINSIR, + Species.TAUROS, + Species.GYARADOS, + Species.LAPRAS, + Species.EEVEE, + Species.VAPOREON, + Species.JOLTEON, + Species.FLAREON, + Species.PORYGON, + Species.OMANYTE, + Species.OMASTAR, + Species.KABUTO, + Species.KABUTOPS, + Species.AERODACTYL, + Species.SNORLAX, + Species.ARTICUNO, + Species.ZAPDOS, + Species.MOLTRES, + Species.DRATINI, + Species.DRAGONAIR, + Species.DRAGONITE, + Species.MEWTWO, + Species.MEW, + Species.CHIKORITA, + Species.BAYLEEF, + Species.MEGANIUM, + Species.CYNDAQUIL, + Species.QUILAVA, + Species.TYPHLOSION, + Species.TOTODILE, + Species.CROCONAW, + Species.FERALIGATR, + Species.SENTRET, + Species.FURRET, + Species.HOOTHOOT, + Species.NOCTOWL, + Species.LEDYBA, + Species.LEDIAN, + Species.SPINARAK, + Species.ARIADOS, + Species.CROBAT, + Species.CHINCHOU, + Species.LANTURN, + Species.PICHU, + Species.CLEFFA, + Species.IGGLYBUFF, + Species.TOGEPI, + Species.TOGETIC, + Species.NATU, + Species.XATU, + Species.MAREEP, + Species.FLAAFFY, + Species.AMPHAROS, + Species.BELLOSSOM, + Species.MARILL, + Species.AZUMARILL, + Species.SUDOWOODO, + Species.POLITOED, + Species.HOPPIP, + Species.SKIPLOOM, + Species.JUMPLUFF, + Species.AIPOM, + Species.SUNKERN, + Species.SUNFLORA, + Species.YANMA, + Species.WOOPER, + Species.QUAGSIRE, + Species.ESPEON, + Species.UMBREON, + Species.MURKROW, + Species.SLOWKING, + Species.MISDREAVUS, + Species.GIRAFARIG, + Species.PINECO, + Species.FORRETRESS, + Species.DUNSPARCE, + Species.GLIGAR, + Species.STEELIX, + Species.SNUBBULL, + Species.GRANBULL, + Species.QWILFISH, + Species.SCIZOR, + Species.SHUCKLE, + Species.HERACROSS, + Species.SNEASEL, + Species.TEDDIURSA, + Species.URSARING, + Species.SLUGMA, + Species.MAGCARGO, + Species.SWINUB, + Species.PILOSWINE, + Species.CORSOLA, + Species.REMORAID, + Species.OCTILLERY, + Species.DELIBIRD, + Species.MANTINE, + Species.SKARMORY, + Species.HOUNDOUR, + Species.HOUNDOOM, + Species.KINGDRA, + Species.PHANPY, + Species.DONPHAN, + Species.PORYGON2, + Species.STANTLER, + Species.TYROGUE, + Species.HITMONTOP, + Species.SMOOCHUM, + Species.ELEKID, + Species.MAGBY, + Species.MILTANK, + Species.BLISSEY, + Species.RAIKOU, + Species.ENTEI, + Species.SUICUNE, + Species.LARVITAR, + Species.PUPITAR, + Species.TYRANITAR, + Species.LUGIA, + Species.HO_OH, + Species.CELEBI, + Species.TREECKO, + Species.GROVYLE, + Species.SCEPTILE, + Species.TORCHIC, + Species.COMBUSKEN, + Species.BLAZIKEN, + Species.MUDKIP, + Species.MARSHTOMP, + Species.SWAMPERT, + Species.POOCHYENA, + Species.MIGHTYENA, + Species.ZIGZAGOON, + Species.LINOONE, + Species.BEAUTIFLY, + Species.DUSTOX, + Species.LOTAD, + Species.LOMBRE, + Species.LUDICOLO, + Species.SEEDOT, + Species.NUZLEAF, + Species.SHIFTRY, + Species.TAILLOW, + Species.SWELLOW, + Species.WINGULL, + Species.PELIPPER, + Species.RALTS, + Species.KIRLIA, + Species.GARDEVOIR, + Species.SURSKIT, + Species.MASQUERAIN, + Species.SHROOMISH, + Species.BRELOOM, + Species.SLAKOTH, + Species.VIGOROTH, + Species.SLAKING, + Species.NINCADA, + Species.NINJASK, + Species.SHEDINJA, + Species.WHISMUR, + Species.LOUDRED, + Species.EXPLOUD, + Species.MAKUHITA, + Species.HARIYAMA, + Species.AZURILL, + Species.NOSEPASS, + Species.SKITTY, + Species.DELCATTY, + Species.SABLEYE, + Species.MAWILE, + Species.ARON, + Species.LAIRON, + Species.AGGRON, + Species.MEDITITE, + Species.MEDICHAM, + Species.ELECTRIKE, + Species.MANECTRIC, + Species.PLUSLE, + Species.MINUN, + Species.VOLBEAT, + Species.ILLUMISE, + Species.ROSELIA, + Species.GULPIN, + Species.SWALOT, + Species.CARVANHA, + Species.SHARPEDO, + Species.WAILMER, + Species.WAILORD, + Species.NUMEL, + Species.CAMERUPT, + Species.TORKOAL, + Species.SPOINK, + Species.GRUMPIG, + Species.SPINDA, + Species.TRAPINCH, + Species.VIBRAVA, + Species.FLYGON, + Species.CACNEA, + Species.CACTURNE, + Species.SWABLU, + Species.ALTARIA, + Species.ZANGOOSE, + Species.SEVIPER, + Species.LUNATONE, + Species.SOLROCK, + Species.BARBOACH, + Species.WHISCASH, + Species.CORPHISH, + Species.CRAWDAUNT, + Species.BALTOY, + Species.CLAYDOL, + Species.LILEEP, + Species.CRADILY, + Species.ANORITH, + Species.ARMALDO, + Species.FEEBAS, + Species.MILOTIC, + Species.CASTFORM, + Species.KECLEON, + Species.SHUPPET, + Species.BANETTE, + Species.DUSKULL, + Species.DUSCLOPS, + Species.TROPIUS, + Species.CHIMECHO, + Species.ABSOL, + Species.SNORUNT, + Species.GLALIE, + Species.SPHEAL, + Species.SEALEO, + Species.WALREIN, + Species.CLAMPERL, + Species.HUNTAIL, + Species.GOREBYSS, + Species.RELICANTH, + Species.LUVDISC, + Species.BAGON, + Species.SHELGON, + Species.SALAMENCE, + Species.METANG, + Species.METAGROSS, + Species.REGIROCK, + Species.REGICE, + Species.REGISTEEL, + Species.LATIAS, + Species.LATIOS, + Species.KYOGRE, + Species.GROUDON, + Species.RAYQUAZA, + Species.JIRACHI, + Species.DEOXYS, + Species.TURTWIG, + Species.GROTLE, + Species.TORTERRA, + Species.CHIMCHAR, + Species.MONFERNO, + Species.INFERNAPE, + Species.PIPLUP, + Species.PRINPLUP, + Species.EMPOLEON, + Species.STARLY, + Species.STARAVIA, + Species.STARAPTOR, + Species.BIDOOF, + Species.BIBAREL, + Species.KRICKETUNE, + Species.SHINX, + Species.LUXIO, + Species.LUXRAY, + Species.BUDEW, + Species.ROSERADE, + Species.CRANIDOS, + Species.RAMPARDOS, + Species.SHIELDON, + Species.BASTIODON, + Species.WORMADAM, + Species.MOTHIM, + Species.VESPIQUEN, + Species.PACHIRISU, + Species.BUIZEL, + Species.FLOATZEL, + Species.CHERUBI, + Species.CHERRIM, + Species.SHELLOS, + Species.GASTRODON, + Species.AMBIPOM, + Species.DRIFLOON, + Species.DRIFBLIM, + Species.BUNEARY, + Species.LOPUNNY, + Species.MISMAGIUS, + Species.HONCHKROW, + Species.GLAMEOW, + Species.PURUGLY, + Species.CHINGLING, + Species.STUNKY, + Species.SKUNTANK, + Species.BRONZOR, + Species.BRONZONG, + Species.BONSLY, + Species.MIME_JR, + Species.HAPPINY, + Species.CHATOT, + Species.SPIRITOMB, + Species.GIBLE, + Species.GABITE, + Species.GARCHOMP, + Species.MUNCHLAX, + Species.RIOLU, + Species.LUCARIO, + Species.HIPPOPOTAS, + Species.HIPPOWDON, + Species.SKORUPI, + Species.DRAPION, + Species.CROAGUNK, + Species.TOXICROAK, + Species.CARNIVINE, + Species.FINNEON, + Species.LUMINEON, + Species.MANTYKE, + Species.SNOVER, + Species.ABOMASNOW, + Species.WEAVILE, + Species.MAGNEZONE, + Species.LICKILICKY, + Species.RHYPERIOR, + Species.TANGROWTH, + Species.ELECTIVIRE, + Species.MAGMORTAR, + Species.TOGEKISS, + Species.YANMEGA, + Species.LEAFEON, + Species.GLACEON, + Species.GLISCOR, + Species.MAMOSWINE, + Species.PORYGON_Z, + Species.GALLADE, + Species.PROBOPASS, + Species.DUSKNOIR, + Species.FROSLASS, + Species.ROTOM, + Species.UXIE, + Species.MESPRIT, + Species.AZELF, + Species.DIALGA, + Species.PALKIA, + Species.HEATRAN, + Species.REGIGIGAS, + Species.GIRATINA, + Species.CRESSELIA, + Species.PHIONE, + Species.MANAPHY, + Species.DARKRAI, + Species.SHAYMIN, + Species.ARCEUS, + Species.VICTINI, + Species.SNIVY, + Species.SERVINE, + Species.SERPERIOR, + Species.TEPIG, + Species.PIGNITE, + Species.EMBOAR, + Species.OSHAWOTT, + Species.DEWOTT, + Species.SAMUROTT, + Species.PATRAT, + Species.WATCHOG, + Species.LILLIPUP, + Species.HERDIER, + Species.STOUTLAND, + Species.PURRLOIN, + Species.LIEPARD, + Species.PANSAGE, + Species.SIMISAGE, + Species.PANSEAR, + Species.SIMISEAR, + Species.PANPOUR, + Species.SIMIPOUR, + Species.MUNNA, + Species.MUSHARNA, + Species.PIDOVE, + Species.TRANQUILL, + Species.UNFEZANT, + Species.BLITZLE, + Species.ZEBSTRIKA, + Species.ROGGENROLA, + Species.BOLDORE, + Species.GIGALITH, + Species.WOOBAT, + Species.SWOOBAT, + Species.DRILBUR, + Species.EXCADRILL, + Species.AUDINO, + Species.TIMBURR, + Species.GURDURR, + Species.CONKELDURR, + Species.TYMPOLE, + Species.PALPITOAD, + Species.SEISMITOAD, + Species.THROH, + Species.SAWK, + Species.SEWADDLE, + Species.SWADLOON, + Species.LEAVANNY, + Species.VENIPEDE, + Species.WHIRLIPEDE, + Species.SCOLIPEDE, + Species.COTTONEE, + Species.WHIMSICOTT, + Species.PETILIL, + Species.LILLIGANT, + Species.BASCULIN, + Species.SANDILE, + Species.KROKOROK, + Species.KROOKODILE, + Species.DARUMAKA, + Species.DARMANITAN, + Species.MARACTUS, + Species.DWEBBLE, + Species.CRUSTLE, + Species.SCRAGGY, + Species.SCRAFTY, + Species.SIGILYPH, + Species.YAMASK, + Species.COFAGRIGUS, + Species.TIRTOUGA, + Species.CARRACOSTA, + Species.ARCHEN, + Species.ARCHEOPS, + Species.TRUBBISH, + Species.GARBODOR, + Species.ZORUA, + Species.ZOROARK, + Species.MINCCINO, + Species.CINCCINO, + Species.GOTHITA, + Species.GOTHORITA, + Species.GOTHITELLE, + Species.SOLOSIS, + Species.DUOSION, + Species.REUNICLUS, + Species.DUCKLETT, + Species.SWANNA, + Species.VANILLITE, + Species.VANILLISH, + Species.VANILLUXE, + Species.DEERLING, + Species.SAWSBUCK, + Species.EMOLGA, + Species.KARRABLAST, + Species.ESCAVALIER, + Species.FOONGUS, + Species.AMOONGUSS, + Species.FRILLISH, + Species.JELLICENT, + Species.ALOMOMOLA, + Species.JOLTIK, + Species.GALVANTULA, + Species.FERROSEED, + Species.FERROTHORN, + Species.KLINK, + Species.KLANG, + Species.KLINKLANG, + Species.EELEKTRIK, + Species.EELEKTROSS, + Species.ELGYEM, + Species.BEHEEYEM, + Species.LITWICK, + Species.LAMPENT, + Species.CHANDELURE, + Species.AXEW, + Species.FRAXURE, + Species.HAXORUS, + Species.CUBCHOO, + Species.BEARTIC, + Species.CRYOGONAL, + Species.SHELMET, + Species.ACCELGOR, + Species.STUNFISK, + Species.MIENFOO, + Species.MIENSHAO, + Species.DRUDDIGON, + Species.GOLETT, + Species.GOLURK, + Species.PAWNIARD, + Species.BISHARP, + Species.BOUFFALANT, + Species.RUFFLET, + Species.BRAVIARY, + Species.VULLABY, + Species.MANDIBUZZ, + Species.HEATMOR, + Species.DURANT, + Species.DEINO, + Species.ZWEILOUS, + Species.HYDREIGON, + Species.LARVESTA, + Species.VOLCARONA, + Species.COBALION, + Species.TERRAKION, + Species.VIRIZION, + Species.TORNADUS, + Species.THUNDURUS, + Species.RESHIRAM, + Species.ZEKROM, + Species.LANDORUS, + Species.KYUREM, + Species.KELDEO, + Species.MELOETTA, + Species.GENESECT, + Species.CHESPIN, + Species.QUILLADIN, + Species.CHESNAUGHT, + Species.FENNEKIN, + Species.BRAIXEN, + Species.DELPHOX, + Species.FROAKIE, + Species.FROGADIER, + Species.GRENINJA, + Species.BUNNELBY, + Species.DIGGERSBY, + Species.FLETCHLING, + Species.FLETCHINDER, + Species.TALONFLAME, + Species.VIVILLON, + Species.LITLEO, + Species.PYROAR, + Species.FLABEBE, + Species.FLOETTE, + Species.FLORGES, + Species.SKIDDO, + Species.GOGOAT, + Species.PANCHAM, + Species.PANGORO, + Species.FURFROU, + Species.ESPURR, + Species.MEOWSTIC, + Species.HONEDGE, + Species.DOUBLADE, + Species.AEGISLASH, + Species.SPRITZEE, + Species.AROMATISSE, + Species.SWIRLIX, + Species.SLURPUFF, + Species.INKAY, + Species.MALAMAR, + Species.BINACLE, + Species.BARBARACLE, + Species.SKRELP, + Species.DRAGALGE, + Species.CLAUNCHER, + Species.CLAWITZER, + Species.HELIOPTILE, + Species.HELIOLISK, + Species.TYRUNT, + Species.TYRANTRUM, + Species.AMAURA, + Species.AURORUS, + Species.SYLVEON, + Species.HAWLUCHA, + Species.DEDENNE, + Species.CARBINK, + Species.GOOMY, + Species.SLIGGOO, + Species.GOODRA, + Species.KLEFKI, + Species.PHANTUMP, + Species.TREVENANT, + Species.PUMPKABOO, + Species.GOURGEIST, + Species.BERGMITE, + Species.AVALUGG, + Species.NOIBAT, + Species.NOIVERN, + Species.XERNEAS, + Species.YVELTAL, + Species.ZYGARDE, + Species.DIANCIE, + Species.HOOPA, + Species.VOLCANION, + Species.ROWLET, + Species.DARTRIX, + Species.DECIDUEYE, + Species.LITTEN, + Species.TORRACAT, + Species.INCINEROAR, + Species.POPPLIO, + Species.BRIONNE, + Species.PRIMARINA, + Species.PIKIPEK, + Species.TRUMBEAK, + Species.TOUCANNON, + Species.YUNGOOS, + Species.GUMSHOOS, + Species.GRUBBIN, + Species.CHARJABUG, + Species.VIKAVOLT, + Species.CRABRAWLER, + Species.CRABOMINABLE, + Species.ORICORIO, + Species.CUTIEFLY, + Species.RIBOMBEE, + Species.ROCKRUFF, + Species.LYCANROC, + Species.WISHIWASHI, + Species.MAREANIE, + Species.TOXAPEX, + Species.MUDBRAY, + Species.MUDSDALE, + Species.DEWPIDER, + Species.ARAQUANID, + Species.FOMANTIS, + Species.LURANTIS, + Species.MORELULL, + Species.SHIINOTIC, + Species.SALANDIT, + Species.SALAZZLE, + Species.STUFFUL, + Species.BEWEAR, + Species.BOUNSWEET, + Species.STEENEE, + Species.TSAREENA, + Species.COMFEY, + Species.ORANGURU, + Species.PASSIMIAN, + Species.WIMPOD, + Species.GOLISOPOD, + Species.SANDYGAST, + Species.PALOSSAND, + Species.TYPE_NULL, + Species.SILVALLY, + Species.MINIOR, + Species.KOMALA, + Species.TURTONATOR, + Species.TOGEDEMARU, + Species.MIMIKYU, + Species.BRUXISH, + Species.DRAMPA, + Species.DHELMISE, + Species.JANGMO_O, + Species.HAKAMO_O, + Species.KOMMO_O, + Species.TAPU_KOKO, + Species.TAPU_LELE, + Species.TAPU_BULU, + Species.TAPU_FINI, + Species.SOLGALEO, + Species.LUNALA, + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.NECROZMA, + Species.MAGEARNA, + Species.MARSHADOW, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + Species.ZERAORA, + Species.MELTAN, + Species.MELMETAL, + Species.GROOKEY, + Species.THWACKEY, + Species.RILLABOOM, + Species.SCORBUNNY, + Species.RABOOT, + Species.CINDERACE, + Species.SOBBLE, + Species.DRIZZILE, + Species.INTELEON, + Species.SKWOVET, + Species.GREEDENT, + Species.ROOKIDEE, + Species.CORVISQUIRE, + Species.CORVIKNIGHT, + Species.DOTTLER, + Species.ORBEETLE, + Species.NICKIT, + Species.THIEVUL, + Species.GOSSIFLEUR, + Species.ELDEGOSS, + Species.WOOLOO, + Species.DUBWOOL, + Species.CHEWTLE, + Species.DREDNAW, + Species.YAMPER, + Species.BOLTUND, + Species.ROLYCOLY, + Species.CARKOL, + Species.COALOSSAL, + Species.FLAPPLE, + Species.APPLETUN, + Species.SILICOBRA, + Species.SANDACONDA, + Species.CRAMORANT, + Species.ARROKUDA, + Species.BARRASKEWDA, + Species.TOXEL, + Species.TOXTRICITY, + Species.SIZZLIPEDE, + Species.CENTISKORCH, + Species.CLOBBOPUS, + Species.GRAPPLOCT, + Species.SINISTEA, + Species.POLTEAGEIST, + Species.HATENNA, + Species.HATTREM, + Species.HATTERENE, + Species.IMPIDIMP, + Species.MORGREM, + Species.GRIMMSNARL, + Species.OBSTAGOON, + Species.PERRSERKER, + Species.CURSOLA, + Species.SIRFETCHD, + Species.MR_RIME, + Species.RUNERIGUS, + Species.MILCERY, + Species.ALCREMIE, + Species.FALINKS, + Species.PINCURCHIN, + Species.SNOM, + Species.FROSMOTH, + Species.STONJOURNER, + Species.EISCUE, + Species.INDEEDEE, + Species.MORPEKO, + Species.CUFANT, + Species.COPPERAJAH, + Species.DRACOZOLT, + Species.ARCTOZOLT, + Species.DRACOVISH, + Species.ARCTOVISH, + Species.DURALUDON, + Species.DREEPY, + Species.DRAKLOAK, + Species.DRAGAPULT, + Species.ZACIAN, + Species.ZAMAZENTA, + Species.ETERNATUS, + Species.KUBFU, + Species.URSHIFU, + Species.ZARUDE, + Species.REGIELEKI, + Species.REGIDRAGO, + Species.GLASTRIER, + Species.SPECTRIER, + Species.CALYREX, + Species.WYRDEER, + Species.KLEAVOR, + Species.URSALUNA, + Species.BASCULEGION, + Species.SNEASLER, + Species.OVERQWIL, + Species.ENAMORUS, + Species.SPRIGATITO, + Species.FLORAGATO, + Species.MEOWSCARADA, + Species.FUECOCO, + Species.CROCALOR, + Species.SKELEDIRGE, + Species.QUAXLY, + Species.QUAXWELL, + Species.QUAQUAVAL, + Species.LECHONK, + Species.OINKOLOGNE, + Species.TAROUNTULA, + Species.SPIDOPS, + Species.NYMBLE, + Species.LOKIX, + Species.PAWMI, + Species.PAWMO, + Species.PAWMOT, + Species.TANDEMAUS, + Species.MAUSHOLD, + Species.FIDOUGH, + Species.DACHSBUN, + Species.SMOLIV, + Species.DOLLIV, + Species.ARBOLIVA, + Species.SQUAWKABILLY, + Species.NACLI, + Species.NACLSTACK, + Species.GARGANACL, + Species.CHARCADET, + Species.ARMAROUGE, + Species.CERULEDGE, + Species.TADBULB, + Species.BELLIBOLT, + Species.WATTREL, + Species.KILOWATTREL, + Species.MASCHIFF, + Species.MABOSSTIFF, + Species.SHROODLE, + Species.GRAFAIAI, + Species.BRAMBLIN, + Species.BRAMBLEGHAST, + Species.TOEDSCOOL, + Species.TOEDSCRUEL, + Species.KLAWF, + Species.CAPSAKID, + Species.SCOVILLAIN, + Species.RELLOR, + Species.RABSCA, + Species.FLITTLE, + Species.ESPATHRA, + Species.TINKATINK, + Species.TINKATUFF, + Species.TINKATON, + Species.WIGLETT, + Species.WUGTRIO, + Species.BOMBIRDIER, + Species.FINIZEN, + Species.PALAFIN, + Species.VAROOM, + Species.REVAVROOM, + Species.CYCLIZAR, + Species.ORTHWORM, + Species.GLIMMET, + Species.GLIMMORA, + Species.GREAVARD, + Species.HOUNDSTONE, + Species.FLAMIGO, + Species.CETODDLE, + Species.CETITAN, + Species.VELUZA, + Species.DONDOZO, + Species.TATSUGIRI, + Species.ANNIHILAPE, + Species.CLODSIRE, + Species.FARIGIRAF, + Species.DUDUNSPARCE, + Species.KINGAMBIT, + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.FRIGIBAX, + Species.ARCTIBAX, + Species.BAXCALIBUR, + Species.GIMMIGHOUL, + Species.GHOLDENGO, + Species.WO_CHIEN, + Species.CHIEN_PAO, + Species.TING_LU, + Species.CHI_YU, + Species.ROARING_MOON, + Species.IRON_VALIANT, + Species.KORAIDON, + Species.MIRAIDON, + Species.WALKING_WAKE, + Species.IRON_LEAVES, + Species.DIPPLIN, + Species.POLTCHAGEIST, + Species.SINISTCHA, + Species.OKIDOGI, + Species.MUNKIDORI, + Species.FEZANDIPITI, + Species.OGERPON, + Species.ARCHALUDON, + Species.HYDRAPPLE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_BOULDER, + Species.IRON_CROWN, + Species.TERAPAGOS, + Species.PECHARUNT, + Species.ALOLA_RATTATA, + Species.ALOLA_RATICATE, + Species.ALOLA_RAICHU, + Species.ALOLA_SANDSHREW, + Species.ALOLA_SANDSLASH, + Species.ALOLA_VULPIX, + Species.ALOLA_NINETALES, + Species.ALOLA_DIGLETT, + Species.ALOLA_DUGTRIO, + Species.ALOLA_MEOWTH, + Species.ALOLA_PERSIAN, + Species.ALOLA_GEODUDE, + Species.ALOLA_GRAVELER, + Species.ALOLA_GOLEM, + Species.ALOLA_GRIMER, + Species.ALOLA_MUK, + Species.ALOLA_EXEGGUTOR, + Species.ALOLA_MAROWAK, + Species.ETERNAL_FLOETTE, + Species.GALAR_MEOWTH, + Species.GALAR_PONYTA, + Species.GALAR_RAPIDASH, + Species.GALAR_SLOWPOKE, + Species.GALAR_SLOWBRO, + Species.GALAR_FARFETCHD, + Species.GALAR_WEEZING, + Species.GALAR_MR_MIME, + Species.GALAR_ARTICUNO, + Species.GALAR_ZAPDOS, + Species.GALAR_MOLTRES, + Species.GALAR_SLOWKING, + Species.GALAR_CORSOLA, + Species.GALAR_ZIGZAGOON, + Species.GALAR_LINOONE, + Species.GALAR_DARUMAKA, + Species.GALAR_DARMANITAN, + Species.GALAR_YAMASK, + Species.GALAR_STUNFISK, + Species.HISUI_GROWLITHE, + Species.HISUI_ARCANINE, + Species.HISUI_VOLTORB, + Species.HISUI_ELECTRODE, + Species.HISUI_TYPHLOSION, + Species.HISUI_QWILFISH, + Species.HISUI_SNEASEL, + Species.HISUI_SAMUROTT, + Species.HISUI_LILLIGANT, + Species.HISUI_ZORUA, + Species.HISUI_ZOROARK, + Species.HISUI_BRAVIARY, + Species.HISUI_SLIGGOO, + Species.HISUI_GOODRA, + Species.HISUI_AVALUGG, + Species.HISUI_DECIDUEYE, + Species.PALDEA_TAUROS, + Species.PALDEA_WOOPER, + Species.BLOODMOON_URSALUNA, + ], [Moves.DIVE]: [ Species.SQUIRTLE, Species.WARTORTLE, @@ -59529,26 +60592,6 @@ export const tmSpecies: TmSpecies = { Species.HISUI_SAMUROTT, Species.PALDEA_TAUROS, ], - [Moves.SACRED_SWORD]: [ - Species.GALLADE, - Species.OSHAWOTT, - Species.DEWOTT, - Species.SAMUROTT, - Species.COBALION, - Species.TERRAKION, - Species.VIRIZION, - Species.KELDEO, - Species.HONEDGE, - Species.DOUBLADE, - Species.AEGISLASH, - Species.KARTANA, - Species.ZACIAN, - Species.CHIEN_PAO, - Species.IRON_LEAVES, - Species.IRON_BOULDER, - Species.IRON_CROWN, - Species.HISUI_SAMUROTT, - ], [Moves.RAZOR_SHELL]: [ Species.SLOWBRO, Species.SHELLDER, @@ -67389,6 +68432,7 @@ export const tmPoolTiers: TmPoolTiers = { [Moves.ENDEAVOR]: ModifierTier.COMMON, [Moves.SKILL_SWAP]: ModifierTier.COMMON, [Moves.IMPRISON]: ModifierTier.COMMON, + [Moves.SECRET_POWER]: ModifierTier.COMMON, [Moves.DIVE]: ModifierTier.GREAT, [Moves.FEATHER_DANCE]: ModifierTier.COMMON, [Moves.BLAZE_KICK]: ModifierTier.GREAT, @@ -67517,7 +68561,6 @@ export const tmPoolTiers: TmPoolTiers = { [Moves.ELECTROWEB]: ModifierTier.GREAT, [Moves.WILD_CHARGE]: ModifierTier.GREAT, [Moves.DRILL_RUN]: ModifierTier.GREAT, - [Moves.SACRED_SWORD]: ModifierTier.ULTRA, [Moves.RAZOR_SHELL]: ModifierTier.GREAT, [Moves.HEAT_CRASH]: ModifierTier.GREAT, [Moves.TAIL_SLAP]: ModifierTier.GREAT, diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 37900a3ab5a..5928a65b4e3 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -428,7 +428,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { moveAnim.bgSprite.setScale(1.25); moveAnim.bgSprite.setAlpha(this.opacity / 255); scene.field.add(moveAnim.bgSprite); - const fieldPokemon = scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon(); + const fieldPokemon = scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false); if (!isNullOrUndefined(priority)) { scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority); } else if (fieldPokemon?.isOnField()) { @@ -999,7 +999,7 @@ export abstract class BattleAnim { const setSpritePriority = (priority: integer) => { switch (priority) { case 0: - scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getNonSwitchedEnemyPokemon() || scene.getNonSwitchedPlayerPokemon()!); // This bang assumes that if (the EnemyPokemon is undefined, then the PlayerPokemon function must return an object), correct assumption? + scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getEnemyPokemon(false) ?? scene.getPlayerPokemon(false)!); // TODO: is this bang correct? break; case 1: scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index d671c56ab26..3051c2061a8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1573,6 +1573,22 @@ export class AbilityBattlerTag extends BattlerTag { } } +/** + * Tag used by Unburden to double speed + * @extends AbilityBattlerTag + */ +export class UnburdenTag extends AbilityBattlerTag { + constructor() { + super(BattlerTagType.UNBURDEN, Abilities.UNBURDEN, BattlerTagLapseType.CUSTOM, 1); + } + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + } + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + } +} + export class TruantTag extends AbilityBattlerTag { constructor() { super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1); @@ -2934,6 +2950,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new ThroatChoppedTag(); case BattlerTagType.GORILLA_TACTICS: return new GorillaTacticsTag(); + case BattlerTagType.UNBURDEN: + return new UnburdenTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); case BattlerTagType.AUTOTOMIZED: diff --git a/src/data/berry.ts b/src/data/berry.ts index 7243c4c1b2e..d2bbd0fdd1c 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -2,7 +2,7 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { HitResult } from "../field/pokemon"; import { getStatusEffectHealText } from "./status-effect"; import * as Utils from "../utils"; -import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability"; +import { DoubleBerryEffectAbAttr, PostItemLostAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs, applyPostItemLostAbAttrs } from "./ability"; import i18next from "i18next"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -75,6 +75,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LUM: return (pokemon: Pokemon) => { @@ -86,6 +87,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { } pokemon.resetStatus(true, true); pokemon.updateInfo(); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LIECHI: case BerryType.GANLON: @@ -101,6 +103,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const statStages = new Utils.NumberHolder(1); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LANSAT: return (pokemon: Pokemon) => { @@ -108,6 +111,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { pokemon.battleData.berriesEaten.push(berryType); } pokemon.addTag(BattlerTagType.CRIT_BOOST); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.STARF: return (pokemon: Pokemon) => { @@ -118,6 +122,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const stages = new Utils.NumberHolder(2); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LEPPA: return (pokemon: Pokemon) => { @@ -128,6 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (ppRestoreMove !== undefined) { ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) })); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); } }; } diff --git a/src/data/move.ts b/src/data/move.ts index 9979b24cc24..6e350315e65 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger { POST_TARGET, } +interface MoveEffectAttrOptions { + /** + * Defines when this effect should trigger in the move's effect order + * @see {@linkcode MoveEffectPhase} + */ + trigger?: MoveEffectTrigger; + /** Should this effect only apply on the first hit? */ + firstHitOnly?: boolean; + /** Should this effect only apply on the last hit? */ + lastHitOnly?: boolean; + /** Should this effect only apply on the first target hit? */ + firstTargetOnly?: boolean; + /** Overrides the secondary effect chance for this attr if set. */ + effectChanceOverride?: number; +} + /** Base class defining all Move Effect Attributes * @extends MoveAttr * @see {@linkcode apply} */ export class MoveEffectAttr extends MoveAttr { - /** Defines when this effect should trigger in the move's effect order - * @see {@linkcode phases.MoveEffectPhase.start} + /** + * A container for this attribute's optional parameters + * @see {@linkcode MoveEffectAttrOptions} for supported params. */ - public trigger: MoveEffectTrigger; - /** Should this effect only apply on the first hit? */ - public firstHitOnly: boolean; - /** Should this effect only apply on the last hit? */ - public lastHitOnly: boolean; - /** Should this effect only apply on the first target hit? */ - public firstTargetOnly: boolean; - /** Overrides the secondary effect chance for this attr if set. */ - public effectChanceOverride?: number; + protected options?: MoveEffectAttrOptions; - constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) { + constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { super(selfTarget); - this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY; - this.firstHitOnly = firstHitOnly; - this.lastHitOnly = lastHitOnly; - this.firstTargetOnly = firstTargetOnly; - this.effectChanceOverride = effectChanceOverride; + this.options = options; + } + + /** + * Defines when this effect should trigger in the move's effect order. + * @default MoveEffectTrigger.POST_APPLY + * @see {@linkcode MoveEffectTrigger} + */ + public get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY; + } + + /** + * `true` if this effect should only trigger on the first hit of + * multi-hit moves. + * @default false + */ + public get firstHitOnly () { + return this.options?.firstHitOnly ?? false; + } + + /** + * `true` if this effect should only trigger on the last hit of + * multi-hit moves. + * @default false + */ + public get lastHitOnly () { + return this.options?.lastHitOnly ?? false; + } + + /** + * `true` if this effect should apply only upon hitting a target + * for the first time when targeting multiple {@linkcode Pokemon}. + * @default false + */ + public get firstTargetOnly () { + return this.options?.firstTargetOnly ?? false; + } + + /** + * If defined, overrides the move's base chance for this + * secondary effect to trigger. + */ + public get effectChanceOverride () { + return this.options?.effectChanceOverride; } /** @@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr { private unblockable: boolean; constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY, false, true); + super(true, { lastHitOnly: true }); this.useHp = useHp; this.damageRatio = damageRatio; @@ -1420,8 +1469,13 @@ export class RecoilAttr extends MoveEffectAttr { return false; } - const damageValue = (!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio; - const minValue = user.turnData.damageDealt ? 1 : 0; + // Chloroblast and Struggle should not deal recoil damage if the move was not successful + if (this.useHp && [ MoveResult.FAIL, MoveResult.MISS ].includes(user.getLastXMoves(1)[0]?.result)) { + return false; + } + + const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio; + const minValue = user.turnData.totalDamageDealt ? 1 : 0; const recoilDamage = Utils.toDmgValue(damageValue, minValue); if (!recoilDamage) { return false; @@ -1451,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr { **/ export class SacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1484,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr { **/ export class SacrificialAttrOnHit extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** @@ -1523,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { */ export class HalfSacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1689,7 +1743,7 @@ export class PartyStatusCureAttr extends MoveEffectAttr { if (!this.canApply(user, target, move, args)) { return false; } - const partyPokemon = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const partyPokemon = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); partyPokemon.forEach(p => this.cureStatus(p, user.id)); if (this.message) { @@ -1761,7 +1815,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { } // We don't know which party member will be chosen, so pick the highest max HP in the party - const maxPartyMemberHp = user.scene.getParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0); + const maxPartyMemberHp = user.scene.getPlayerParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0); user.scene.pushPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), maxPartyMemberHp, i18next.t("moveTriggers:sacrificialFullRestore", { pokemonName: getPokemonNameWithAffix(user) }), true, false, false, true), true); @@ -1774,7 +1828,7 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => user.scene.getParty().filter(p => p.isActive()).length > user.scene.currentBattle.getBattlerCount(); + return (user, _target, _move) => user.scene.getPlayerParty().filter(p => p.isActive()).length > user.scene.currentBattle.getBattlerCount(); } } @@ -1927,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr { private healStat: EffectiveStat | null; constructor(healRatio?: number | null, healStat?: EffectiveStat) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.healRatio = healRatio ?? 0.5; this.healStat = healStat ?? null; @@ -1952,7 +2006,7 @@ export class HitHealAttr extends MoveEffectAttr { message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) }); } else { // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. - healAmount = Utils.toDmgValue(user.turnData.currDamageDealt * this.healRatio); + healAmount = Utils.toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio); message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); } if (reverseDrain) { @@ -2104,7 +2158,7 @@ export class MultiHitAttr extends MoveAttr { case MultiHitType._10: return 10; case MultiHitType.BEAT_UP: - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); // No status means the ally pokemon can contribute to Beat Up return party.reduce((total, pokemon) => { return total + (pokemon.id === user.id ? 1 : pokemon?.status && pokemon.status.effect !== StatusEffect.NONE ? 0 : 1); @@ -2136,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr { public overrideStatus: boolean = false; constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; this.turnsRemaining = turnsRemaining; @@ -2177,7 +2231,10 @@ export class StatusEffectAttr extends MoveEffectAttr { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; + const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1); + const pokemon = this.selfTarget ? user : target; + + return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; } } @@ -2197,13 +2254,16 @@ export class MultiStatusEffectAttr extends StatusEffectAttr { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; + const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1); + const pokemon = this.selfTarget ? user : target; + + return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; } } export class PsychoShiftEffectAttr extends MoveEffectAttr { constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2228,7 +2288,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(user.status?.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; + return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0; } } /** @@ -2240,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { private chance: number; constructor(chance: number) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.chance = chance; } @@ -2301,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { private berriesOnly: boolean; constructor(berriesOnly: boolean) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.berriesOnly = berriesOnly; } @@ -2342,6 +2402,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon !--removedItem.stackCount; target.scene.updateModifiers(target.isPlayer()); + applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); + if (this.berriesOnly) { user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); @@ -2375,7 +2437,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr { protected chosenBerry: BerryModifier | undefined; constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** * Causes the target to eat a berry. @@ -2421,6 +2483,7 @@ export class EatBerryAttr extends MoveEffectAttr { eatBerry(consumer: Pokemon) { getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); // consumer eats the berry applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, consumer, false); } } @@ -2456,6 +2519,7 @@ export class StealEatBerryAttr extends EatBerryAttr { } // if the target has berries, pick a random berry and steal it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; + applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); user.scene.queueMessage(message); this.reduceBerryModifier(target); @@ -2478,7 +2542,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { * @param ...effects - List of status effects to cure */ constructor(selfTarget: boolean, ...effects: StatusEffect[]) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true); + super(selfTarget, { lastHitOnly: true }); this.effects = effects; } @@ -2808,35 +2872,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { } } +/** + * Set of optional parameters that may be applied to stat stage changing effects + * @extends MoveEffectAttrOptions + * @see {@linkcode StatStageChangeAttr} + */ +interface StatStageChangeAttrOptions extends MoveEffectAttrOptions { + /** If defined, needs to be met in order for the stat change to apply */ + condition?: MoveConditionFunc, + /** `true` to display a message */ + showMessage?: boolean +} + /** * Attribute used for moves that change stat stages * * @param stats {@linkcode BattleStat} Array of stat(s) to change * @param stages How many stages to change the stat(s) by, [-6, 6] * @param selfTarget `true` if the move is self-targetting - * @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes - * @param showMessage `true` to display a message; default `true` - * @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false` - * @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT} - * @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false` - * @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false` - * @param effectChanceOverride Will override the move's normal secondary effect chance if specified + * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute. * * @extends MoveEffectAttr * @see {@linkcode apply} */ export class StatStageChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; - public stages: integer; - private condition?: MoveConditionFunc | null; - private showMessage: boolean; + public stages: number; + /** + * Container for optional parameters to this attribute. + * @see {@linkcode StatStageChangeAttrOptions} for available optional params + */ + protected override options?: StatStageChangeAttrOptions; - constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) { - super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride); + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) { + super(selfTarget, options); this.stats = stats; this.stages = stages; - this.condition = condition; - this.showMessage = showMessage; + this.options = options; + } + + /** + * The condition required for the stat stage change to apply. + * Defaults to `null` (i.e. no condition required). + */ + private get condition () { + return this.options?.condition ?? null; + } + + /** + * `true` to display a message for the stat change. + * @default true + */ + private get showMessage () { + return this.options?.showMessage ?? true; + } + + /** + * Indicates when the stat change should trigger + * @default MoveEffectTrigger.HIT + */ + public override get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.HIT; } /** @@ -2921,20 +3017,6 @@ export class SecretPowerAttr extends MoveEffectAttr { super(false); } - /** - * Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance - * @returns `true` if the move's secondary effect should apply - */ - override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - this.effectChanceOverride = move.chance; - const moveChance = this.getMoveChance(user, target, move, this.selfTarget); - if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { - return true; - } else { - return false; - } - } - /** * Used to apply the secondary effect to the target Pokemon * @returns `true` if a secondary effect is successfully applied @@ -2951,8 +3033,6 @@ export class SecretPowerAttr extends MoveEffectAttr { const biome = user.scene.arena.biomeType; secondaryEffect = this.determineBiomeEffect(biome); } - // effectChanceOverride used in the application of the actual secondary effect - secondaryEffect.effectChanceOverride = 100; return secondaryEffect.apply(user, target, move, []); } @@ -3128,7 +3208,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { - super(stat, levels, true, null, true); + super(stat, levels, true); this.cutRatio = cutRatio; this.messageCallback = messageCallback; @@ -3391,7 +3471,7 @@ export class MovePowerMultiplierAttr extends VariablePowerAttr { * @returns The base power of the Beat Up hit. */ const beatUpFunc = (user: Pokemon, allyIndex: number): number => { - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); for (let i = allyIndex; i < party.length; i++) { const pokemon = party[i]; @@ -3419,7 +3499,7 @@ export class BeatUpAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const power = args[0] as Utils.NumberHolder; - const party = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); const allyCount = party.filter(pokemon => { return pokemon.id === user.id || !pokemon.status?.effect; }).length; @@ -4081,6 +4161,60 @@ export class CombinedPledgeStabBoostAttr extends MoveAttr { } } +/** + * Variable Power attribute for {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}. + * Doubles power if another Pokemon has previously selected Round this turn. + * @extends VariablePowerAttr + */ +export class RoundPowerAttr extends VariablePowerAttr { + override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const power = args[0]; + if (!(power instanceof Utils.NumberHolder)) { + return false; + } + + if (user.turnData?.joinedRound) { + power.value *= 2; + return true; + } + return false; + } +} + +/** + * Attribute for the "combo" effect of {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}. + * Preempts the next move in the turn order with the first instance of any Pokemon + * using Round. Also marks the Pokemon using the cued Round to double the move's power. + * @extends MoveEffectAttr + * @see {@linkcode RoundPowerAttr} + */ +export class CueNextRoundAttr extends MoveEffectAttr { + constructor() { + super(true, { lastHitOnly: true }); + } + + override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + const nextRoundPhase = user.scene.findPhase(phase => + phase instanceof MovePhase && phase.move.moveId === Moves.ROUND + ); + + if (!nextRoundPhase) { + return false; + } + + // Update the phase queue so that the next Pokemon using Round moves next + const nextRoundIndex = user.scene.phaseQueue.indexOf(nextRoundPhase); + const nextMoveIndex = user.scene.phaseQueue.findIndex(phase => phase instanceof MovePhase); + if (nextRoundIndex !== nextMoveIndex) { + user.scene.prependToPhase(user.scene.phaseQueue.splice(nextRoundIndex, 1)[0], MovePhase); + } + + // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) + nextRoundPhase.pokemon.turnData.joinedRound = true; + return true; + } +} + export class VariableAtkAttr extends MoveAttr { constructor() { super(); @@ -4389,7 +4523,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr { } if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) { - const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct? + const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; moveType.value = Type[Type[form]]; return true; @@ -4878,7 +5012,7 @@ export class BypassRedirectAttr extends MoveAttr { export class FrenzyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, false, true); + super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true }); } canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { @@ -4951,7 +5085,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { private failOnOverlap: boolean; constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly); + super(selfTarget, { lastHitOnly: lastHitOnly }); this.tagType = tagType; this.turnCountMin = turnCountMin; @@ -5386,7 +5520,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagType = tagType; this.turnCount = turnCount!; // TODO: is the bang correct? @@ -5424,7 +5558,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagTypes = tagTypes; this.selfSideTarget = selfSideTarget; @@ -5490,7 +5624,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5526,7 +5660,7 @@ export class RemoveScreensAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5564,7 +5698,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr { constructor(SwapTags: ArenaTagType[]) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.SwapTags = SwapTags; } @@ -5644,7 +5778,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { return new Promise(resolve => { // If user is player, checks if the user has fainted pokemon if (user instanceof PlayerPokemon - && user.scene.getParty().findIndex(p => p.isFainted()) > -1) { + && user.scene.getPlayerParty().findIndex(p => p.isFainted()) > -1) { (user as PlayerPokemon).revivalBlessing().then(() => { resolve(true); }); @@ -5685,12 +5819,13 @@ export class RevivalBlessingAttr extends MoveEffectAttr { } } + export class ForceSwitchOutAttr extends MoveEffectAttr { constructor( private selfSwitch: boolean = false, private switchType: SwitchType = SwitchType.SWITCH ) { - super(false, MoveEffectTrigger.POST_APPLY, false, true); + super(false, { lastHitOnly: true }); } isBatonPass() { @@ -5703,14 +5838,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } - /** - * Move the switch out logic inside the conditional block - * This ensures that the switch out only happens when the conditions are met - */ const switchOutTarget = this.selfSwitch ? user : target; if (switchOutTarget instanceof PlayerPokemon) { + /** + * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch + * If it did, the user of U-turn or Volt Switch will not be switched out. + */ + if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && + (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN) + ) { + if (this.hpDroppedBelowHalf(target)) { + return false; + } + } // Switch out logic for the player's Pokemon - if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } @@ -5734,11 +5876,27 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { false, false), MoveEndPhase); } } else { + /** + * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch + * If it did, the user of U-turn or Volt Switch will not be switched out. + */ + if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && + (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) { + if (this.hpDroppedBelowHalf(target)) { + return false; + } + } + // Switch out logic for everything else (eg: WILD battles) if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; } + // Don't allow wild mons to flee with U-turn et al + if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) { + return false; + } + if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(false); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); @@ -5803,7 +5961,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } } - const party = player ? user.scene.getParty() : user.scene.getEnemyParty(); + const party = player ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); return (!player && !user.scene.currentBattle.battleType) || party.filter(p => p.isAllowedInBattle() && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > user.scene.currentBattle.getBattlerCount(); @@ -5821,8 +5979,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } return ret; } -} + /** + * Helper function to check if the Pokémon's health is below half after taking damage. + * Used for an edge case interaction with Wimp Out/Emergency Exit. + * If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out. + */ + hpDroppedBelowHalf(target: Pokemon): boolean { + const pokemonHealth = target.hp; + const maxPokemonHealth = target.getMaxHp(); + const damageTaken = target.turnData.damageTaken; + const initialHealth = pokemonHealth + damageTaken; + + // Check if the Pokémon's health has dropped below half after the damage + return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2; + } +} export class ChillyReceptionAttr extends ForceSwitchOutAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -5841,7 +6013,7 @@ export class RemoveTypeAttr extends MoveEffectAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); this.removedType = removedType; this.messageCallback = messageCallback; @@ -5858,6 +6030,9 @@ export class RemoveTypeAttr extends MoveEffectAttr { const userTypes = user.getTypes(true); const modifiedTypes = userTypes.filter(type => type !== this.removedType); + if (modifiedTypes.length === 0) { + modifiedTypes.push(Type.UNKNOWN); + } user.summonData.types = modifiedTypes; user.updateInfo(); @@ -5880,7 +6055,11 @@ export class CopyTypeAttr extends MoveEffectAttr { return false; } - user.summonData.types = target.getTypes(true); + const targetTypes = target.getTypes(true); + if (targetTypes.includes(Type.UNKNOWN) && targetTypes.indexOf(Type.UNKNOWN) > -1) { + targetTypes[targetTypes.indexOf(Type.UNKNOWN)] = Type.NORMAL; + } + user.summonData.types = targetTypes; user.updateInfo(); user.scene.queueMessage(i18next.t("moveTriggers:copyType", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target) })); @@ -5889,7 +6068,7 @@ export class CopyTypeAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN; + return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN || target.summonData.addedType !== null; } } @@ -5903,22 +6082,114 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { return false; } - const biomeType = user.scene.arena.getTypeForBiome(); + const terrainType = user.scene.arena.getTerrainType(); + let typeChange: Type; + if (terrainType !== TerrainType.NONE) { + typeChange = this.getTypeForTerrain(user.scene.arena.getTerrainType()); + } else { + typeChange = this.getTypeForBiome(user.scene.arena.biomeType); + } - user.summonData.types = [ biomeType ]; + user.summonData.types = [ typeChange ]; user.updateInfo(); - user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`) })); + user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[typeChange]}`) })); return true; } + + /** + * Retrieves a type from the current terrain + * @param terrainType {@linkcode TerrainType} + * @returns {@linkcode Type} + */ + private getTypeForTerrain(terrainType: TerrainType): Type { + switch (terrainType) { + case TerrainType.ELECTRIC: + return Type.ELECTRIC; + case TerrainType.MISTY: + return Type.FAIRY; + case TerrainType.GRASSY: + return Type.GRASS; + case TerrainType.PSYCHIC: + return Type.PSYCHIC; + case TerrainType.NONE: + default: + return Type.UNKNOWN; + } + } + + /** + * Retrieves a type from the current biome + * @param biomeType {@linkcode Biome} + * @returns {@linkcode Type} + */ + private getTypeForBiome(biomeType: Biome): Type { + switch (biomeType) { + case Biome.TOWN: + case Biome.PLAINS: + case Biome.METROPOLIS: + return Type.NORMAL; + case Biome.GRASS: + case Biome.TALL_GRASS: + return Type.GRASS; + case Biome.FOREST: + case Biome.JUNGLE: + return Type.BUG; + case Biome.SLUM: + case Biome.SWAMP: + return Type.POISON; + case Biome.SEA: + case Biome.BEACH: + case Biome.LAKE: + case Biome.SEABED: + return Type.WATER; + case Biome.MOUNTAIN: + return Type.FLYING; + case Biome.BADLANDS: + return Type.GROUND; + case Biome.CAVE: + case Biome.DESERT: + return Type.ROCK; + case Biome.ICE_CAVE: + case Biome.SNOWY_FOREST: + return Type.ICE; + case Biome.MEADOW: + case Biome.FAIRY_CAVE: + case Biome.ISLAND: + return Type.FAIRY; + case Biome.POWER_PLANT: + return Type.ELECTRIC; + case Biome.VOLCANO: + return Type.FIRE; + case Biome.GRAVEYARD: + case Biome.TEMPLE: + return Type.GHOST; + case Biome.DOJO: + case Biome.CONSTRUCTION_SITE: + return Type.FIGHTING; + case Biome.FACTORY: + case Biome.LABORATORY: + return Type.STEEL; + case Biome.RUINS: + case Biome.SPACE: + return Type.PSYCHIC; + case Biome.WASTELAND: + case Biome.END: + return Type.DRAGON; + case Biome.ABYSS: + return Type.DARK; + default: + return Type.UNKNOWN; + } + } } export class ChangeTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } @@ -5941,17 +6212,13 @@ export class AddTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const types = target.getTypes().slice(0, 2).filter(t => t !== Type.UNKNOWN); // TODO: Figure out some way to actually check if another version of this effect is already applied - if (this.type !== Type.UNKNOWN) { - types.push(this.type); - } - target.summonData.types = types; + target.summonData.addedType = this.type; target.updateInfo(); user.scene.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`), pokemonName: getPokemonNameWithAffix(target) })); @@ -6472,7 +6739,7 @@ export class AbilityChangeAttr extends MoveEffectAttr { public ability: Abilities; constructor(ability: Abilities, selfTarget?: boolean) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.ability = ability; } @@ -6501,7 +6768,7 @@ export class AbilityCopyAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor(copyToPartner: boolean = false) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.copyToPartner = copyToPartner; } @@ -6540,7 +6807,7 @@ export class AbilityGiveAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -6852,7 +7119,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr { export class MoneyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, true); + super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true }); } apply(user: Pokemon, target: Pokemon, move: Move): boolean { @@ -6869,7 +7136,7 @@ export class MoneyAttr extends MoveEffectAttr { */ export class DestinyBondAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); } /** @@ -6919,7 +7186,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { public effect: StatusEffect; constructor(effect: StatusEffect) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; } @@ -7044,6 +7311,11 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; +const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { + const party: Pokemon[] = user.isPlayer() ? user.scene.getPlayerParty() : user.scene.getEnemyParty(); + return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); +}; + export type MoveAttrFilter = (attr: MoveAttr) => boolean; function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise { @@ -7953,6 +8225,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS) + .condition(failIfLastInPartyCondition) .hidesUser(), new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) @@ -8741,8 +9014,9 @@ export function initMoves() { .condition((user, target, move) => !target.turnData.acted) .attr(AfterYouAttr), new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) - .soundBased() - .partial(), // No effect implemented + .attr(CueNextRoundAttr) + .attr(RoundPowerAttr) + .soundBased(), new AttackMove(Moves.ECHOED_VOICE, Type.NORMAL, MoveCategory.SPECIAL, 40, 100, 15, -1, 0, 5) .attr(ConsecutiveUseMultiBasePowerAttr, 5, false) .soundBased(), @@ -8969,9 +9243,9 @@ export function initMoves() { .target(MoveTarget.ALL) .condition((user, target, move) => { // If any fielded pokémon is grass-type and grounded. - return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); + return [ ...user.scene.getEnemyParty(), ...user.scene.getPlayerParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); }) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .target(MoveTarget.ENEMY_SIDE), @@ -8983,8 +9257,7 @@ export function initMoves() { .ignoresProtect() .ignoresVirtual(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GHOST) - .edgeCase(), // Weird interaction with Forest's Curse, reflect type, burn up + .attr(AddTypeAttr, Type.GHOST), new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .soundBased(), @@ -8996,8 +9269,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS) .triageMove(), new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GRASS) - .edgeCase(), // Weird interaction with Trick or Treat, reflect type, burn up + .attr(AddTypeAttr, Type.GRASS), new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) .windMove() .makesContact(false) @@ -9010,7 +9282,7 @@ export function initMoves() { .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(ForceSwitchOutAttr, true) .soundBased(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) @@ -9025,7 +9297,7 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) - .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), @@ -9045,8 +9317,9 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6) .ignoresSubstitute() + .ignoresProtect() .target(MoveTarget.BOTH_SIDES) - .unimplemented(), + .attr(AddArenaTagAttr, ArenaTagType.FAIRY_LOCK, 2, true), new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD) .condition(failIfLastCondition), @@ -9057,7 +9330,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) @@ -9083,7 +9356,7 @@ export function initMoves() { new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .ignoresSubstitute() @@ -9094,7 +9367,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9314,7 +9587,7 @@ export function initMoves() { new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9371,7 +9644,7 @@ export function initMoves() { .ballBombMove() .makesContact(false), new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), @@ -9485,13 +9758,13 @@ export function initMoves() { .makesContact(false) .ignoresVirtual(), new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) .edgeCase() // I assume it needs clanging scales and Kommo-O .ignoresVirtual(), /* End Unused */ - new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) //LGPE Implementation + new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) // LGPE Implementation .attr(CritOnlyAttr), new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) @@ -9501,7 +9774,7 @@ export function initMoves() { new AttackMove(Moves.PIKA_PAPOW, Type.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 20, -1, 0, 7) .attr(FriendshipPowerAttr), new AttackMove(Moves.BOUNCY_BUBBLE, Type.WATER, MoveCategory.SPECIAL, 60, 100, 20, -1, 0, 7) - .attr(HitHealAttr, 1.0) + .attr(HitHealAttr) // Custom .triageMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BUZZY_BUZZ, Type.ELECTRIC, MoveCategory.SPECIAL, 60, 100, 20, 100, 0, 7) @@ -9723,8 +9996,8 @@ export function initMoves() { .attr(ClearTerrainAttr) .condition((user, target, move) => !!user.scene.arena.terrain), new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) - .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true }) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true }) .attr(MultiHitAttr) .makesContact(false), new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) @@ -9858,7 +10131,7 @@ export function initMoves() { new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) .makesContact(false) .attr(HighCritAttr) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 }) .attr(FlinchAttr), new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) .attr(StatusEffectAttr, StatusEffect.BURN) @@ -9994,7 +10267,7 @@ export function initMoves() { .attr(TeraMoveCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }) .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP) @@ -10005,6 +10278,7 @@ export function initMoves() { .attr(ConfuseAttr) .recklessMove(), new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) + .partial() // Counter resets every wave instead of on arena reset .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 100)) .makesContact(false), new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) @@ -10077,7 +10351,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(MoneyAttr) - .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true }) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) @@ -10094,12 +10368,13 @@ export function initMoves() { .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) .attr(AddSubstituteAttr, 0.5) - .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL), + .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL) + .condition(failIfLastInPartyCondition), new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(ChillyReceptionAttr, true), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .attr(RemoveArenaTrapAttr, true) .attr(RemoveAllSubstitutesAttr), new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index c53b802bb22..fa8e1aed1c7 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -181,7 +181,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = // Sort berries by party member ID to more easily re-add later if necessary const berryItemsMap = new Map(); - scene.getParty().forEach(pokemon => { + scene.getPlayerParty().forEach(pokemon => { const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); if (pokemonBerries?.length > 0) { berryItemsMap.set(pokemon.id, pokemonBerries); @@ -267,7 +267,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED); encounter.setDialogueToken("foodReward", revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name")); const givePartyPokemonReviverSeeds = () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party.forEach(p => { const heldItems = p.getHeldItems(); if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) { @@ -308,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = const berryMap = encounter.misc.berryItemsMap; // Returns 2/5 of the berries stolen to each Pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party.forEach(pokemon => { const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); const berryTypesAsArray: BerryType[] = []; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 095f8a8473b..5524511c67b 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -58,7 +58,7 @@ export const BerriesAboundEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { @@ -77,7 +77,7 @@ export const BerriesAboundEncounter: MysteryEncounter = scene.currentBattle.waveIndex > 160 ? 7 : scene.currentBattle.waveIndex > 120 ? 5 : scene.currentBattle.waveIndex > 40 ? 4 : 2; - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); encounter.misc = { numBerries }; const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); @@ -253,7 +253,7 @@ function tryGiveBerry(scene: BattleScene, prioritizedPokemon?: PlayerPokemon) { const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType; const berry = generateModifierType(scene, modifierTypes.BERRY, [ berryType ]) as BerryModifierType; - const party = scene.getParty(); + const party = scene.getPlayerParty(); // Will try to apply to prioritized pokemon first, then do normal application method if it fails if (prioritizedPokemon) { diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index e24eadb56c7..f2605795955 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -331,7 +331,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Player gets different rewards depending on the number of bug types they have - const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; + const numBugTypes = scene.getPlayerParty().filter(p => p.isOfType(Type.BUG, true)).length; const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, { count: numBugTypes }); encounter.setDialogueToken("numBugTypes", numBugTypesText); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index c4b03660bde..5502cc7b53a 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -1,7 +1,7 @@ import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; @@ -245,7 +245,7 @@ export const ClowningAroundEncounter: MysteryEncounter = // So Vitamins, form change items, etc. are not included const encounter = scene.currentBattle.mysteryEncounter!; - const party = scene.getParty(); + const party = scene.getPlayerParty(); let mostHeldItemsPokemon = party[0]; let count = mostHeldItemsPokemon.getHeldItems() .filter(m => m.isTransferable && !(m instanceof BerryModifier)) @@ -280,7 +280,7 @@ export const ClowningAroundEncounter: MysteryEncounter = let numRogue = 0; items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .forEach(m => { - const type = m.type.withTierFromPool(); + const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); const tier = type.tier ?? ModifierTier.ULTRA; if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { numRogue += m.stackCount; @@ -328,7 +328,7 @@ export const ClowningAroundEncounter: MysteryEncounter = .withPreOptionPhase(async (scene: BattleScene) => { // Randomize the second type of all player's pokemon // If the pokemon does not normally have a second type, it will gain 1 - for (const pokemon of scene.getParty()) { + for (const pokemon of scene.getPlayerParty()) { const originalTypes = pokemon.getTypes(false, false, true); // If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 55d7ce0e92d..bae5a8790e9 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -1,32 +1,32 @@ -import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; +import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { Moves } from "#enums/moves"; -import { TrainerSlot } from "#app/data/trainer-config"; -import PokemonData from "#app/system/pokemon-data"; -import { Biome } from "#enums/biome"; -import { EncounterBattleAnim } from "#app/data/battle-anims"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; -import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; -import { BattlerIndex } from "#app/battle"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { PokeballType } from "#enums/pokeball"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { TrainerSlot } from "#app/data/trainer-config"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { modifierTypes } from "#app/modifier/modifier-type"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; -import { Stat } from "#enums/stat"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Biome } from "#enums/biome"; import { EncounterAnim } from "#enums/encounter-anims"; -import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PokeballType } from "#enums/pokeball"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import i18next from "i18next"; /** the i18n namespace for this encounter */ @@ -92,7 +92,7 @@ export const DancingLessonsEncounter: MysteryEncounter = .withCatchAllowed(true) .withFleeAllowed(false) .withOnVisualsStart((scene: BattleScene) => { - const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getParty()[0]); + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()!); danceAnim.play(scene); return true; @@ -217,7 +217,7 @@ export const DancingLessonsEncounter: MysteryEncounter = const onPokemonSelected = (pokemon: PlayerPokemon) => { encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); - scene.unshiftPhase(new LearnMovePhase(scene, scene.getParty().indexOf(pokemon), Moves.REVELATION_DANCE)); + scene.unshiftPhase(new LearnMovePhase(scene, scene.getPlayerParty().indexOf(pokemon), Moves.REVELATION_DANCE)); // Play animation again to "learn" the dance const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()); diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index d5f9388b56c..d5a938b9cef 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -1,22 +1,22 @@ -import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; -import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import i18next from "#app/plugins/i18n"; -import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { getPokemonSpecies } from "#app/data/pokemon-species"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { BerryModifier, HealingBoosterModifier, LevelIncrementBoosterModifier, MoneyMultiplierModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import i18next from "#app/plugins/i18n"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/delibirdy"; @@ -133,7 +133,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { @@ -207,7 +207,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { @@ -220,7 +220,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerPokemon()!, shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { @@ -299,7 +299,7 @@ export const DelibirdyEncounter: MysteryEncounter = if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; - await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + await applyModifierTypeToPlayerPokemon(scene, scene.getPlayerParty()[0], shellBell); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index bf5fb28163b..1c26df0cf71 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -214,7 +214,7 @@ function pokemonAndMoveChosen(scene: BattleScene, pokemon: PlayerPokemon, move: text: `${namespace}:incorrect_exp`, }, ]; - setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + setEncounterExp(scene, scene.getPlayerParty().map((p) => p.id), 50); } else { encounter.selectedOption!.dialogue!.selected = [ { diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 5c16e5d8564..5794277ffe1 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -184,7 +184,7 @@ export const FieryFalloutEncounter: MysteryEncounter = async (scene: BattleScene) => { // Damage non-fire types and burn 1 random non-fire type member + give it Heatproof const encounter = scene.currentBattle.mysteryEncounter!; - const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); + const nonFireTypes = scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); for (const pkm of nonFireTypes) { const percentage = DAMAGE_PERCENTAGE / 100; @@ -257,7 +257,7 @@ export const FieryFalloutEncounter: MysteryEncounter = function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) { // Give first party pokemon attack type boost item for free at end of battle - const leadPokemon = scene.getParty()?.[0]; + const leadPokemon = scene.getPlayerParty()?.[0]; if (leadPokemon) { // Generate type booster held item, default to Charcoal if item fails to generate let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 889868519d2..3f9030dc3b2 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -56,7 +56,7 @@ export const FightOrFlightEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); const config: EnemyPartyConfig = { @@ -86,11 +86,11 @@ export const FightOrFlightEncounter: MysteryEncounter = : scene.currentBattle.waveIndex > 40 ? ModifierTier.ULTRA : ModifierTier.GREAT; - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { - item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; + item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; } encounter.setDialogueToken("itemName", item.type.name); encounter.misc = item; diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index b843a929c08..7bf48aa5926 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -165,7 +165,7 @@ async function summonPlayerPokemon(scene: BattleScene) { const playerPokemon = encounter.misc.playerPokemon; // Swaps the chosen Pokemon and the first player's lead Pokemon in the party - const party = scene.getParty(); + const party = scene.getPlayerParty(); const chosenIndex = party.indexOf(playerPokemon); if (chosenIndex !== 0) { const leadPokemon = party[0]; diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 376bdf0c95d..1d65fd6c9a6 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -191,7 +191,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = receivedPokemonData.pokeball = randInt(4) as PokeballType; const dataSource = new PokemonData(receivedPokemonData); const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); - scene.getParty().push(newPlayerPokemon); + scene.getPlayerParty().push(newPlayerPokemon); await newPlayerPokemon.loadAssets(); for (const mod of modifiers) { @@ -224,7 +224,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Randomly generate a Wonder Trade pokemon - const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); + const randomTradeOption = generateTradeOption(scene.getPlayerParty().map(p => p.species)); const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false); // Extra shiny roll at 1/128 odds (boosted by events and charms) if (!tradePokemon.shiny) { @@ -299,7 +299,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = receivedPokemonData.pokeball = randInt(4) as PokeballType; const dataSource = new PokemonData(receivedPokemonData); const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); - scene.getParty().push(newPlayerPokemon); + scene.getPlayerParty().push(newPlayerPokemon); await newPlayerPokemon.loadAssets(); for (const mod of modifiers) { @@ -384,11 +384,11 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = tier++; } - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + regenerateModifierPoolThresholds(scene.getPlayerParty(), ModifierPoolType.PLAYER, 0); let item: ModifierTypeOption | null = null; // TMs excluded from possible rewards while (!item || item.type.id.includes("TM_")) { - item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; + item = getPlayerModifierTypeOptions(1, scene.getPlayerParty(), [], { guaranteedModifierTiers: [ tier ], allowLuckUpgrades: false })[0]; } encounter.setDialogueToken("itemName", item.type.name); @@ -430,9 +430,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = function getPokemonTradeOptions(scene: BattleScene): Map { const tradeOptionsMap: Map = new Map(); // Starts by filtering out any current party members as valid resulting species - const alreadyUsedSpecies: PokemonSpecies[] = scene.getParty().map(p => p.species); + const alreadyUsedSpecies: PokemonSpecies[] = scene.getPlayerParty().map(p => p.species); - scene.getParty().forEach(pokemon => { + scene.getPlayerParty().forEach(pokemon => { // If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { const generation = pokemon.species.generation; diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 8e7ea52a967..a8cb076bbe9 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -104,7 +104,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with ], }, async (scene: BattleScene) => { - const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle()); + const allowedPokemon = scene.getPlayerParty().filter((p) => p.isAllowedInBattle()); for (const pkm of allowedPokemon) { const percentage = DAMAGE_PERCENTAGE / 100; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 693d935ae17..ab6517e97af 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -1,19 +1,19 @@ -import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { randSeedInt } from "#app/utils"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { Species } from "#enums/species"; -import { Moves } from "#enums/moves"; -import { GameOverPhase } from "#app/phases/game-over-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { randSeedInt } from "#app/utils"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/mysteriousChest"; @@ -177,7 +177,7 @@ export const MysteriousChestEncounter: MysteryEncounter = await showEncounterText(scene, `${namespace}:option.1.bad`); // Handle game over edge case - const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle()); + const allowedPokemon = scene.getPokemonAllowedInBattle(); if (allowedPokemon.length === 0) { // If there are no longer any legal pokemon in the party, game over. scene.clearPhaseQueue(); diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 8dd730492b1..798e85868f6 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -100,7 +100,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = // Only Pokemon that can gain benefits are above half HP with no status const selectableFilter = (pokemon: Pokemon) => { // If pokemon meets primary pokemon reqs, it can be selected - if (!pokemon.isAllowed()) { + if (!pokemon.isAllowedInChallenge()) { return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; } if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) { diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index e8f11f02e18..faeba95f358 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -134,7 +134,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = // Init enemy const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { @@ -170,7 +170,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { // Init enemy const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 610209f8aad..7c904bd047a 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -126,7 +126,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = ]; // Determine the 3 pokemon the player can battle with - let partyCopy = scene.getParty().slice(0); + let partyCopy = scene.getPlayerParty().slice(0); partyCopy = partyCopy .filter(p => p.isAllowedInBattle()) .sort((a, b) => a.friendship - b.friendship); @@ -508,11 +508,11 @@ function getEggOptions(scene: BattleScene, commonEggs: number, rareEggs: number) } function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) { - const party = scene.getParty(); + const party = scene.getPlayerParty(); const chosenIndex = party.indexOf(chosenPokemon); party[chosenIndex] = party[0]; party[0] = chosenPokemon; - encounter.misc.originalParty = scene.getParty().slice(1); + encounter.misc.originalParty = scene.getPlayerParty().slice(1); encounter.misc.originalPartyHeldItems = encounter.misc.originalParty .map(p => p.getHeldItems()); scene["party"] = [ @@ -529,7 +529,7 @@ function checkAchievement(scene: BattleScene) { function restorePartyAndHeldItems(scene: BattleScene) { const encounter = scene.currentBattle.mysteryEncounter!; // Restore original party - scene.getParty().push(...encounter.misc.originalParty); + scene.getPlayerParty().push(...encounter.misc.originalParty); // Restore held items const originalHeldItems = encounter.misc.originalPartyHeldItems; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 397d2af9522..b1ef6cedb21 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -140,7 +140,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = // -15 to all base stats of highest BST (halved for HP), +10 to all base stats of rest of party (halved for HP) // Sort party by bst - const sortedParty = scene.getParty().slice(0) + const sortedParty = scene.getPlayerParty().slice(0) .sort((pokemon1, pokemon2) => { const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0); const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0); diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index bf322802f81..7d9b531c9ab 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -189,7 +189,7 @@ function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { const playerField = scene.getPlayerField(); playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); - for (const pokemon of scene.getParty()) { + for (const pokemon of scene.getPlayerParty()) { // Only trigger form change when Eiscue is in Noice form // Hardcoded Eiscue for now in case it is fused with another pokemon if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) { diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 03341a713f2..8814eb0d8cf 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -152,7 +152,7 @@ export const TrainingSessionEncounter: MysteryEncounter = } // Add pokemon and mods back - scene.getParty().push(playerPokemon); + scene.getPlayerParty().push(playerPokemon); for (const mod of modifiers.value) { mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); @@ -229,7 +229,7 @@ export const TrainingSessionEncounter: MysteryEncounter = scene.gameData.setPokemonCaught(playerPokemon, false); // Add pokemon and modifiers back - scene.getParty().push(playerPokemon); + scene.getPlayerParty().push(playerPokemon); for (const mod of modifiers.value) { mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); @@ -342,7 +342,7 @@ export const TrainingSessionEncounter: MysteryEncounter = scene.gameData.setPokemonCaught(playerPokemon, false); // Add pokemon and mods back - scene.getParty().push(playerPokemon); + scene.getPlayerParty().push(playerPokemon); for (const mod of modifiers.value) { mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index d3c16ce2122..fba3a6ca95e 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -164,7 +164,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) { const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; const leftovers = generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType; - const party = scene.getParty(); + const party = scene.getPlayerParty(); // Iterate over the party until an item was successfully given // First leftovers diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index b7ce3bab48c..a2c32c6af40 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -51,7 +51,7 @@ export const UncommonBreedEncounter: MysteryEncounter = // Calculate boss mon // Level equal to 2 below highest party member const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; - const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true); // Pokemon will always have one of its egg moves in its moveset diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 2ecba6ce658..c05e707ed4d 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -176,7 +176,7 @@ export const WeirdDreamEncounter: MysteryEncounter = for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) { scene.removePokemonFromPlayerParty(transformation.previousPokemon, false); - scene.getParty().push(transformation.newPokemon); + scene.getPlayerParty().push(transformation.newPokemon); } }) .withOptionPhase(async (scene: BattleScene) => { @@ -280,7 +280,7 @@ export const WeirdDreamEncounter: MysteryEncounter = const onBeforeRewards = () => { // Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) // One random pokemon will get its passive unlocked - const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; @@ -306,7 +306,7 @@ export const WeirdDreamEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Leave, reduce party levels by 10% - for (const pokemon of scene.getParty()) { + for (const pokemon of scene.getPlayerParty()) { pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.levelExp = 0; @@ -329,7 +329,7 @@ interface PokemonTransformation { } function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { - const party = scene.getParty(); + const party = scene.getPlayerParty(); // Removes all pokemon from the party const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species); const pokemonTransformations: PokemonTransformation[] = party.map(p => { @@ -404,7 +404,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon if (shouldGetOldGateau(newPokemon)) { const stats = getOldGateauBoostedStats(newPokemon); const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() - .generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ]) + .generateType(scene.getPlayerParty(), [ OLD_GATEAU_STATS_UP, stats ]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); const modifier = modType?.newModifier(newPokemon); if (modifier) { @@ -417,7 +417,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon } // One random pokemon will get its passive unlocked - const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + const passiveDisabledPokemon = scene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index ffae71b9555..c674ebdc46e 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -88,7 +88,7 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { * @param pokemon */ pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { - return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getPlayerParty()).map(p => p.id).includes(pokemon.id)); } /** @@ -102,10 +102,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.primaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - const queryParty = req.queryParty(scene.getParty()); + const queryParty = req.queryParty(scene.getPlayerParty()); qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); } else { this.primaryPokemon = undefined; @@ -162,10 +162,10 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption { return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.secondaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - const queryParty = req.queryParty(scene.getParty()); + const queryParty = req.queryParty(scene.getPlayerParty()); qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); } else { this.secondaryPokemon = []; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 91ea0c5be19..1358c465d17 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,21 +1,21 @@ -import { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; -import { isNullOrUndefined } from "#app/utils"; -import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; -import { Species } from "#enums/species"; -import { TimeOfDay } from "#enums/time-of-day"; -import { Nature } from "#app/data/nature"; +import { allAbilities } from "#app/data/ability"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import { Nature } from "#app/data/nature"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; import { StatusEffect } from "#app/data/status-effect"; import { Type } from "#app/data/type"; import { WeatherType } from "#app/data/weather"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PlayerPokemon } from "#app/field/pokemon"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; +import { isNullOrUndefined } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; import { SpeciesFormKey } from "#enums/species-form-key"; -import { allAbilities } from "#app/data/ability"; +import { TimeOfDay } from "#enums/time-of-day"; export interface EncounterRequirement { meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met @@ -333,7 +333,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement { override meetsRequirement(scene: BattleScene): boolean { if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) { - const partySize = this.excludeDisallowedPokemon ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; + const partySize = this.excludeDisallowedPokemon ? scene.getPokemonAllowedInBattle().length : scene.getPlayerParty().length; if (partySize >= 0 && (this.partySizeRange[0] >= 0 && this.partySizeRange[0] > partySize) || (this.partySizeRange[1] >= 0 && this.partySizeRange[1] < partySize)) { return false; } @@ -343,7 +343,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement { } override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - return [ "partySize", scene.getParty().length.toString() ]; + return [ "partySize", scene.getPlayerParty().length.toString() ]; } } @@ -358,7 +358,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { return false; } @@ -421,7 +421,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { return false; } @@ -459,7 +459,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { return false; } @@ -498,7 +498,7 @@ export class TypeRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - let partyPokemon = scene.getParty(); + let partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon)) { return false; @@ -545,7 +545,7 @@ export class MoveRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { return false; } @@ -594,7 +594,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { return false; } @@ -635,7 +635,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { return false; } @@ -677,7 +677,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { return false; } @@ -746,7 +746,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { return false; } @@ -798,7 +798,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) { return false; } @@ -849,7 +849,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon)) { return false; } @@ -900,7 +900,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); if (isNullOrUndefined(partyPokemon)) { return false; } @@ -957,7 +957,7 @@ export class LevelRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon inside required level range if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; @@ -995,7 +995,7 @@ export class FriendshipRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon inside required friendship range if (!isNullOrUndefined(this.requiredFriendshipRange) && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; @@ -1038,7 +1038,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon's health inside required health range if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; @@ -1082,7 +1082,7 @@ export class WeightRequirement extends EncounterPokemonRequirement { override meetsRequirement(scene: BattleScene): boolean { // Party Pokemon's weight inside required weight range if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { - const partyPokemon = scene.getParty(); + const partyPokemon = scene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { return false; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index c045ee51bd7..badbfcf7a56 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -314,7 +314,7 @@ export default class MysteryEncounter implements IMysteryEncounter { * @param pokemon */ pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { - return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getPlayerParty()).map(p => p.id).includes(pokemon.id)); } /** @@ -326,18 +326,18 @@ export default class MysteryEncounter implements IMysteryEncounter { */ private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { - const activeMon = scene.getParty().filter(p => p.isActive(true)); + const activeMon = scene.getPlayerParty().filter(p => p.isActive(true)); if (activeMon.length > 0) { this.primaryPokemon = activeMon[0]; } else { - this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0]; + this.primaryPokemon = scene.getPlayerParty().filter(p => p.isAllowedInBattle())[0]; } return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.primaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + qualified = qualified.filter(pkmn => req.queryParty(scene.getPlayerParty()).includes(pkmn)); } else { this.primaryPokemon = undefined; return false; @@ -394,10 +394,10 @@ export default class MysteryEncounter implements IMysteryEncounter { return true; } - let qualified: PlayerPokemon[] = scene.getParty(); + let qualified: PlayerPokemon[] = scene.getPlayerParty(); for (const req of this.secondaryPokemonRequirements) { if (req.meetsRequirement(scene)) { - qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + qualified = qualified.filter(pkmn => req.queryParty(scene.getPlayerParty()).includes(pkmn)); } else { this.secondaryPokemon = []; return false; diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index f34d383dbbc..a2c08938fbe 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -39,7 +39,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { } override meetsRequirement(scene: BattleScene): boolean { - const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle())); + const partyPokemon = scene.getPlayerParty().filter((pkm) => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle())); if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { return false; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 5cd2fbffd5f..429b6bfa292 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -418,9 +418,9 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie // Populates item id and tier (order matters) result = result .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(); + .withTierFromPool(ModifierPoolType.PLAYER, scene.getPlayerParty()); - return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; + return result instanceof ModifierTypeGenerator ? result.generateType(scene.getPlayerParty(), pregenArgs) : result; } /** @@ -451,9 +451,9 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p // Open party screen to choose pokemon scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { - if (slotIndex < scene.getParty().length) { + if (slotIndex < scene.getPlayerParty().length) { scene.ui.setMode(modeToSetOnExit).then(() => { - const pokemon = scene.getParty()[slotIndex]; + const pokemon = scene.getPlayerParty()[slotIndex]; const secondaryOptions = onPokemonSelected(pokemon); if (!secondaryOptions) { scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); @@ -563,7 +563,7 @@ export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelec const selectPokemonAfterOption = (selectedOptionIndex: number) => { // Open party screen to choose a Pokemon scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { - if (slotIndex < scene.getParty().length) { + if (slotIndex < scene.getPlayerParty().length) { // Pokemon and option selected scene.ui.setMode(modeToSetOnExit).then(() => { const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex }; @@ -713,7 +713,7 @@ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: bo * @param doNotContinue - default `false`. If set to true, will not end the battle and continue to next wave */ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { - const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + const allowedPkm = scene.getPlayerParty().filter((pkm) => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { scene.clearPhaseQueue(); @@ -750,7 +750,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: * @param addHealPhase */ export function handleMysteryEncounterBattleFailed(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { - const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + const allowedPkm = scene.getPlayerParty().filter((pkm) => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { scene.clearPhaseQueue(); diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index b1adc478ab0..dd42cc9ce55 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -53,24 +53,24 @@ export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string, } /** - * Will never remove the player's last non-fainted Pokemon (if they only have 1) + * Will never remove the player's last non-fainted Pokemon (if they only have 1). * Otherwise, picks a Pokemon completely at random and removes from the party * @param scene - * @param isAllowed Default false. If true, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true), will return a mon that is not allowed. - * @param isFainted Default false. If true, includes fainted mons. - * @param doNotReturnLastAllowedMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) + * @param isAllowed Default `false`. If `true`, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true`), will return a mon that is not allowed. + * @param isFainted Default `false`. If `true`, includes fainted mons. + * @param doNotReturnLastAllowedMon Default `false`. If `true`, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) * @returns */ export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false, doNotReturnLastAllowedMon: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let chosenIndex: number; let chosenPokemon: PlayerPokemon | null = null; - const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && (isFainted || !p.isFainted())); - const allowedOnlyMons = party.filter(p => p.isAllowed()); + const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && (isFainted || !p.isFainted())); + const allowedOnlyMons = party.filter(p => p.isAllowedInChallenge()); if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) { // If there is only 1 legal/unfainted mon left, select from fainted legal mons - const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && p.isFainted()); + const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowedInChallenge()) && p.isFainted()); if (faintedLegalMons.length > 0) { chosenIndex = randSeedInt(faintedLegalMons.length); chosenPokemon = faintedLegalMons[chosenIndex]; @@ -101,11 +101,11 @@ export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = * @returns */ export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -127,11 +127,11 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: bool * @returns */ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -152,11 +152,11 @@ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentS * @returns */ export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -177,11 +177,11 @@ export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boole * @returns */ export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); + const party = scene.getPlayerParty(); let pokemon: PlayerPokemon | null = null; for (const p of party) { - if (isAllowed && !p.isAllowed()) { + if (isAllowed && !p.isAllowedInChallenge()) { continue; } if (!isFainted && p.isFainted()) { @@ -315,7 +315,7 @@ export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, h */ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE() - .generateType(pokemon.scene.getParty(), [ value ]) + .generateType(pokemon.scene.getPlayerParty(), [ value ]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); const modifier = modType?.newModifier(pokemon); if (modifier) { @@ -591,7 +591,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po const addToParty = (slotIndex?: number) => { const newPokemon = pokemon.addToParty(pokeballType, slotIndex); const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); - if (scene.getParty().filter(p => p.isShiny()).length === 6) { + if (scene.getPlayerParty().filter(p => p.isShiny()).length === 6) { scene.validateAchv(achvs.SHINY_PARTY); } Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { @@ -605,7 +605,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po }); }; Promise.all([ pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon) ]).then(() => { - if (scene.getParty().length === 6) { + if (scene.getPlayerParty().length === 6) { const promptRelease = () => { scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); @@ -826,7 +826,7 @@ export async function addPokemonDataToDexAndValidateAchievements(scene: BattleSc * @param invalidSelectionKey */ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scene: BattleScene, invalidSelectionKey: string): string | null { - if (!pokemon.isAllowed()) { + if (!pokemon.isAllowedInChallenge()) { return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null; } if (!pokemon.isAllowedInBattle()) { diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index a93c35829ea..107fa5073af 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok return allSpecies[species - 1]; } -export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm { +export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm { const retSpecies: PokemonSpecies = species >= 2000 ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? : allSpecies[species - 1]; @@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean; export abstract class PokemonSpeciesForm { public speciesId: Species; - public formIndex: integer; - public generation: integer; - public type1: Type; - public type2: Type | null; - public height: number; - public weight: number; - public ability1: Abilities; - public ability2: Abilities; - public abilityHidden: Abilities; - public baseTotal: integer; - public baseStats: integer[]; - public catchRate: integer; - public baseFriendship: integer; - public baseExp: integer; - public genderDiffs: boolean; - public isStarterSelectable: boolean; + protected _formIndex: number; + protected _generation: number; + readonly type1: Type; + readonly type2: Type | null; + readonly height: number; + readonly weight: number; + readonly ability1: Abilities; + readonly ability2: Abilities; + readonly abilityHidden: Abilities; + readonly baseTotal: number; + readonly baseStats: number[]; + readonly catchRate: number; + readonly baseFriendship: number; + readonly baseExp: number; + readonly genderDiffs: boolean; + readonly isStarterSelectable: boolean; constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean + ) { this.type1 = type1; this.type2 = type2; this.height = height; @@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm { return ret; } - isOfType(type: integer): boolean { + get generation(): number { + return this._generation; + } + + set generation(generation: number) { + this._generation = generation; + } + + get formIndex(): number { + return this._formIndex; + } + + set formIndex(formIndex: number) { + this._formIndex = formIndex; + } + + isOfType(type: number): boolean { return this.type1 === type || (this.type2 !== null && this.type2 === type); } @@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm { * Method to get the total number of abilities a Pokemon species has. * @returns Number of abilities */ - getAbilityCount(): integer { + getAbilityCount(): number { return this.abilityHidden !== Abilities.NONE ? 3 : 2; } @@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm { * @param abilityIndex Which ability to get (should only be 0-2) * @returns The id of the Ability */ - getAbility(abilityIndex: integer): Abilities { + getAbility(abilityIndex: number): Abilities { let ret: Abilities; if (abilityIndex === 0) { ret = this.ability1; @@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm { return ret; } - getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; } - getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string { + getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string { if (formIndex === undefined || this instanceof PokemonForm) { formIndex = this.formIndex; } @@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm { return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; } - getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; } - abstract getFormSpriteKey(formIndex?: integer): string; + abstract getFormSpriteKey(formIndex?: number): string; /** @@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm { * @param formIndex optional form index for pokemon with different forms * @returns species id if no additional forms, index with formkey if a pokemon with a form */ - getVariantDataIndex(formIndex?: integer) { + getVariantDataIndex(formIndex?: number) { let formkey: string | null = null; - let variantDataIndex: integer | string = this.speciesId; + let variantDataIndex: number | string = this.speciesId; const species = getPokemonSpecies(this.speciesId); if (species.forms.length > 0 && formIndex !== undefined) { formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex); @@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm { return variantDataIndex; } - getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string { + getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string { const variantDataIndex = this.getVariantDataIndex(formIndex); const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]); return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`; } - getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { if (formIndex === undefined) { formIndex = this.formIndex; } @@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm { return ret; } - getCryKey(formIndex?: integer): string { + getCryKey(formIndex?: number): string { let speciesId = this.speciesId; if (this.speciesId > 2000) { switch (this.speciesId) { @@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm { return ret; } - validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean { + validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean { const rootSpeciesId = this.getRootSpeciesId(); for (const moveId of moveset) { if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) { @@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm { return true; } - loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { + loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { return new Promise(resolve => { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); @@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm { return cry; } - generateCandyColors(scene: BattleScene): integer[][] { + generateCandyColors(scene: BattleScene): number[][] { const sourceTexture = scene.textures.get(this.getSpriteKey(false)); const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame]; @@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm { const canvas = document.createElement("canvas"); - const spriteColors: integer[][] = []; + const spriteColors: number[][] = []; const context = canvas.getContext("2d"); const frame = sourceFrame; @@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm { } for (let i = 0; i < pixelData.length; i += 4) { - const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); + const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0); if (!total) { continue; } @@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm { Math.random = originalRandom; - return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]); + return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]); } } export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable { public name: string; - public subLegendary: boolean; - public legendary: boolean; - public mythical: boolean; - public species: string; - public growthRate: GrowthRate; - public malePercent: number | null; - public genderDiffs: boolean; - public canChangeForm: boolean; - public forms: PokemonForm[]; + readonly subLegendary: boolean; + readonly legendary: boolean; + readonly mythical: boolean; + readonly species: string; + readonly growthRate: GrowthRate; + readonly malePercent: number | null; + readonly genderDiffs: boolean; + readonly canChangeForm: boolean; + readonly forms: PokemonForm[]; - constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, + constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null, - genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null, + genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[] + ) { super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, catchRate, baseFriendship, baseExp, genderDiffs, false); this.speciesId = id; @@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali }); } - getName(formIndex?: integer): string { + getName(formIndex?: number): string { if (formIndex !== undefined && this.forms.length) { const form = this.forms[formIndex]; let key: string | null; @@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`); } - getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { + getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0)); } - getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { + getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave); } @@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question * @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. */ - private getStrengthLevelDiff(strength: PartyMemberStrength): integer { + private getStrengthLevelDiff(strength: PartyMemberStrength): number { switch (Math.min(strength, PartyMemberStrength.STRONGER)) { case PartyMemberStrength.WEAKEST: return 60; @@ -705,7 +723,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } } - getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { + getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { const prevolutionLevels = this.getPrevolutionLevels(); if (prevolutionLevels.length) { @@ -847,7 +865,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon - getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { + getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { const ret: EvolutionLevel[] = []; if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { const prevolutionLevels = this.getPrevolutionLevels().reverse(); @@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId); } - getFormSpriteKey(formIndex?: integer) { + getFormSpriteKey(formIndex?: number) { if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) { console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`); formIndex = Math.min(formIndex, this.forms.length - 1); @@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm { private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ]; constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false + ) { super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, - catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey)); + catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey)); this.formName = formName; this.formKey = formKey; - this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null; + this.formSpriteKey = formSpriteKey; } - getFormSpriteKey(_formIndex?: integer) { + getFormSpriteKey(_formIndex?: number) { return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey; } } diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index c73f4ec2ae5..1c62ccb14a6 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -28,4 +28,5 @@ export enum ArenaTagType { FIRE_GRASS_PLEDGE = "FIRE_GRASS_PLEDGE", WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE", GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", + FAIRY_LOCK = "FAIRY_LOCK", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 680dedb93cc..0eace9a1607 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -74,6 +74,7 @@ export enum BattlerTagType { DRAGON_CHEER = "DRAGON_CHEER", NO_RETREAT = "NO_RETREAT", GORILLA_TACTICS = "GORILLA_TACTICS", + UNBURDEN = "UNBURDEN", THROAT_CHOPPED = "THROAT_CHOPPED", TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", diff --git a/src/enums/switch-type.ts b/src/enums/switch-type.ts index b25ba6ad119..752c0902636 100644 --- a/src/enums/switch-type.ts +++ b/src/enums/switch-type.ts @@ -3,6 +3,8 @@ * or {@linkcode SwitchSummonPhase} will carry out. */ export enum SwitchType { + /** Switchout specifically for when combat starts and the player is prompted if they will switch Pokemon */ + INITIAL_SWITCH, /** Basic switchout where the Pokemon to switch in is selected */ SWITCH, /** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */ diff --git a/src/field/arena.ts b/src/field/arena.ts index 7bfdf9a0000..abc2b89569c 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -10,7 +10,14 @@ import Move from "#app/data/move"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { BattlerIndex } from "#app/battle"; import { Terrain, TerrainType } from "#app/data/terrain"; -import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "#app/data/ability"; +import { + applyAbAttrs, + applyPostTerrainChangeAbAttrs, + applyPostWeatherChangeAbAttrs, + PostTerrainChangeAbAttr, + PostWeatherChangeAbAttr, + TerrainEventTypeChangeAbAttr +} from "#app/data/ability"; import Pokemon from "#app/field/pokemon"; import Overrides from "#app/overrides"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; @@ -217,66 +224,6 @@ export class Arena { return 0; } - getTypeForBiome() { - switch (this.biomeType) { - case Biome.TOWN: - case Biome.PLAINS: - case Biome.METROPOLIS: - return Type.NORMAL; - case Biome.GRASS: - case Biome.TALL_GRASS: - return Type.GRASS; - case Biome.FOREST: - case Biome.JUNGLE: - return Type.BUG; - case Biome.SLUM: - case Biome.SWAMP: - return Type.POISON; - case Biome.SEA: - case Biome.BEACH: - case Biome.LAKE: - case Biome.SEABED: - return Type.WATER; - case Biome.MOUNTAIN: - return Type.FLYING; - case Biome.BADLANDS: - return Type.GROUND; - case Biome.CAVE: - case Biome.DESERT: - return Type.ROCK; - case Biome.ICE_CAVE: - case Biome.SNOWY_FOREST: - return Type.ICE; - case Biome.MEADOW: - case Biome.FAIRY_CAVE: - case Biome.ISLAND: - return Type.FAIRY; - case Biome.POWER_PLANT: - return Type.ELECTRIC; - case Biome.VOLCANO: - return Type.FIRE; - case Biome.GRAVEYARD: - case Biome.TEMPLE: - return Type.GHOST; - case Biome.DOJO: - case Biome.CONSTRUCTION_SITE: - return Type.FIGHTING; - case Biome.FACTORY: - case Biome.LABORATORY: - return Type.STEEL; - case Biome.RUINS: - case Biome.SPACE: - return Type.PSYCHIC; - case Biome.WASTELAND: - case Biome.END: - return Type.DRAGON; - case Biome.ABYSS: - return Type.DARK; - default: - return Type.UNKNOWN; - } - } - getBgTerrainColorRatioForBiome(): number { switch (this.biomeType) { case Biome.SPACE: @@ -387,6 +334,7 @@ export class Arena { this.scene.getField(true).filter(p => p.isOnField()).map(pokemon => { pokemon.findAndRemoveTags(t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain)); applyPostTerrainChangeAbAttrs(PostTerrainChangeAbAttr, pokemon, terrain); + applyAbAttrs(TerrainEventTypeChangeAbAttr, pokemon, null, false); }); return true; @@ -786,7 +734,7 @@ export class Arena { case Biome.VOLCANO: return 17.637; case Biome.GRAVEYARD: - return 3.232; + return 13.711; case Biome.DOJO: return 6.205; case Biome.FACTORY: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 278a28937ef..5506c35e18a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -12,7 +12,7 @@ import * as Utils from "#app/utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier"; import { PokeballType } from "#app/data/pokeball"; import { Gender } from "#app/data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; @@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#app/data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -325,35 +325,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.scene.field.getIndex(this) > -1; } - isFainted(checkStatus?: boolean): boolean { - return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT); + /** + * Checks if a pokemon is fainted (ie: its `hp <= 0`). + * It's usually better to call {@linkcode isAllowedInBattle()} + * @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT} + * @returns `true` if the pokemon is fainted + */ + public isFainted(checkStatus: boolean = false): boolean { + return this.hp <= 0 && (!checkStatus || this.status?.effect === StatusEffect.FAINT); } /** - * Check if this pokemon is both not fainted (or a fled wild pokemon) and allowed to be in battle. - * This is frequently a better alternative to {@link isFainted} - * @returns {boolean} True if pokemon is allowed in battle + * Check if this pokemon is both not fainted and allowed to be in battle based on currently active challenges. + * @returns {boolean} `true` if pokemon is allowed in battle */ - isAllowedInBattle(): boolean { - return !this.isFainted() && this.isAllowed(); + public isAllowedInBattle(): boolean { + return !this.isFainted() && this.isAllowedInChallenge(); } /** - * Check if this pokemon is allowed (no challenge exclusion) - * This is frequently a better alternative to {@link isFainted} - * @returns {boolean} True if pokemon is allowed in battle + * Check if this pokemon is allowed based on any active challenges. + * It's usually better to call {@linkcode isAllowedInBattle()} + * @returns {boolean} `true` if pokemon is allowed in battle */ - isAllowed(): boolean { + public isAllowedInChallenge(): boolean { const challengeAllowed = new Utils.BooleanHolder(true); applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); return challengeAllowed.value; } - isActive(onField?: boolean): boolean { + /** + * Checks if the pokemon is allowed in battle (ie: not fainted, and allowed under any active challenges). + * @param onField `true` to also check if the pokemon is currently on the field, defaults to `false` + * @returns `true` if the pokemon is "active". Returns `false` if there is no active {@linkcode BattleScene} + */ + public isActive(onField: boolean = false): boolean { if (!this.scene) { return false; } - return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField()); + return this.isAllowedInBattle() && (!onField || this.isOnField()); } getDexAttr(): bigint { @@ -428,38 +438,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { resolve(); }; if (this.shiny) { - const populateVariantColors = (key: string, back: boolean = false): Promise => { + const populateVariantColors = (isBackSprite: boolean = false): Promise => { return new Promise(resolve => { - const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); + const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); let config = variantData; - const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride)); + const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride)); battleSpritePath.split("/").map(p => config ? config = config[p] : null); const variantSet: VariantSet = config as VariantSet; if (variantSet && variantSet[this.variant] === 1) { - if (variantColorCache.hasOwnProperty(key)) { - return resolve(); + const cacheKey = this.getBattleSpriteKey(isBackSprite); + if (!variantColorCache.hasOwnProperty(cacheKey)) { + this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); } - this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`). - then(res => { - // Prevent the JSON from processing if it failed to load - if (!res.ok) { - console.error(`Could not load ${res.url}!`); - return; - } - return res.json(); - }).then(c => { - variantColorCache[key] = c; - resolve(); - }); - } else { - resolve(); } + resolve(); }); }; if (this.isPlayer()) { - Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve()); + Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve()); } else { - populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve()); + populateVariantColors(false).then(() => updateFusionPaletteAndResolve()); } } else { updateFusionPaletteAndResolve(); @@ -472,6 +470,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } + /** + * Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on + * non-experimental sprites before giving up. + * + * @param cacheKey the cache key for the variant color sprite + * @param attemptedSpritePath the sprite path that failed to load + * @param useExpSprite was the attempted sprite experimental + * @param battleSpritePath the filename of the sprite + * @param optionalParams any additional params to log + */ + fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { + console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams); + if (useExpSprite) { + this.populateVariantColorCache(cacheKey, false, battleSpritePath); + } + } + + /** + * Attempt to process variant sprite. + * + * @param cacheKey the cache key for the variant color sprite + * @param useExpSprite should the experimental sprite be used + * @param battleSpritePath the filename of the sprite + */ + populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { + const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`; + this.scene.cachedFetch(spritePath).then(res => { + // Prevent the JSON from processing if it failed to load + if (!res.ok) { + return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText); + } + return res.json(); + }).catch(error => { + this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); + }).then(c => { + variantColorCache[cacheKey] = c; + }); + } + getFormKey(): string { if (!this.species.forms.length || this.species.forms.length <= this.formIndex) { return ""; @@ -945,6 +982,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } + if (this.getTag(BattlerTagType.UNBURDEN) && !this.scene.getField(true).some(pokemon => pokemon !== this && pokemon.hasAbilityWithAttr(SuppressFieldAbilitiesAbAttr))) { + ret *= 2; + } break; } @@ -1258,6 +1298,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + // the 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)) { + types.push(this.summonData.addedType); + } + // If both types are the same (can happen in weird custom typing scenarios), reduce to single type if (types.length > 1 && types[0] === types[1]) { types.splice(0, 1); @@ -1523,7 +1568,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated) ); - return (trappedByAbility.value || !!this.getTag(TrappedTag)); + const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + return (trappedByAbility.value || !!this.getTag(TrappedTag) || !!this.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side)); } /** @@ -2786,7 +2832,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * We explicitly require to ignore the faint phase here, as we want to show the messages * about the critical hit and the super effective/not very effective messages before the faint phase. */ - const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source); if (damage > 0) { if (source.isPlayer()) { @@ -2795,10 +2841,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.gameData.gameStats.highestDamage = damage; } } - source.turnData.damageDealt += damage; - source.turnData.currDamageDealt = damage; + source.turnData.totalDamageDealt += damage; + source.turnData.singleHitDamageDealt = damage; this.turnData.damageTaken += damage; this.battleData.hitCount++; + + // Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit + if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) { + const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier); + if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) { + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); + } + } + const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); if (source.isPlayer() && !this.isPlayer()) { @@ -2886,7 +2941,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.destroySubstitute(); this.resetSummonData(); } - return damage; } @@ -2900,12 +2954,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ - damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { + damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer { const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); return damage; } @@ -4157,7 +4212,7 @@ export class PlayerPokemon extends Pokemon { return new Promise(resolve => { this.scene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:integer, option: PartyOption) => { if (slotIndex >= 0 && slotIndex < 6) { - const pokemon = this.scene.getParty()[slotIndex]; + const pokemon = this.scene.getPlayerParty()[slotIndex]; if (!pokemon || !pokemon.isFainted()) { resolve(); } @@ -4167,7 +4222,7 @@ export class PlayerPokemon extends Pokemon { pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp())); this.scene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: pokemon.name }), 0, true); - if (this.scene.currentBattle.double && this.scene.getParty().length > 1) { + if (this.scene.currentBattle.double && this.scene.getPlayerParty().length > 1) { const allyPokemon = this.getAlly(); if (slotIndex <= 1) { // Revived ally pokemon @@ -4309,7 +4364,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.fusionLuck = this.fusionLuck; newPokemon.usedTMs = this.usedTMs; - this.scene.getParty().push(newPokemon); + this.scene.getPlayerParty().push(newPokemon); newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, true) as PokemonHeldItemModifier[]; @@ -4425,8 +4480,8 @@ export class PlayerPokemon extends Pokemon { this.generateCompatibleTms(); this.updateInfo(true); - const fusedPartyMemberIndex = this.scene.getParty().indexOf(pokemon); - let partyMemberIndex = this.scene.getParty().indexOf(this); + const fusedPartyMemberIndex = this.scene.getPlayerParty().indexOf(pokemon); + let partyMemberIndex = this.scene.getPlayerParty().indexOf(this); if (partyMemberIndex > fusedPartyMemberIndex) { partyMemberIndex--; } @@ -4439,8 +4494,8 @@ export class PlayerPokemon extends Pokemon { Promise.allSettled(transferModifiers).then(() => { this.scene.updateModifiers(true, true).then(() => { this.scene.removePartyMemberModifiers(fusedPartyMemberIndex); - this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0]; - const newPartyMemberIndex = this.scene.getParty().indexOf(this); + this.scene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; + const newPartyMemberIndex = this.scene.getPlayerParty().indexOf(this); pokemon.getMoveset(true).map((m: PokemonMove) => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id))); pokemon.destroy(); this.updateFusionPalette(); @@ -5031,7 +5086,7 @@ export class EnemyPokemon extends Pokemon { * @returns the pokemon that was added or null if the pokemon could not be added */ addToParty(pokeballType: PokeballType, slotIndex: number = -1) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); let ret: PlayerPokemon | null = null; if (party.length < PLAYER_PARTY_MAX_SIZE) { @@ -5089,7 +5144,6 @@ export class PokemonSummonData { public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; - public speciesForm: PokemonSpeciesForm | null; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; @@ -5100,6 +5154,7 @@ export class PokemonSummonData { public moveset: (PokemonMove | null)[]; // If not initialized this value will not be populated from save data. public types: Type[] = []; + public addedType: Type | null = null; } export class PokemonBattleData { @@ -5128,8 +5183,8 @@ export class PokemonTurnData { * - `0` = Move is finished */ public hitsLeft: number = -1; - public damageDealt: number = 0; - public currDamageDealt: number = 0; + public totalDamageDealt: number = 0; + public singleHitDamageDealt: number = 0; public damageTaken: number = 0; public attacksReceived: AttackMoveResult[] = []; public order: number; @@ -5137,6 +5192,9 @@ export class PokemonTurnData { public statStagesDecreased: boolean = false; public moveEffectiveness: TypeDamageMultiplier | null = null; public combiningPledge?: Moves; + public switchedInThisTurn: boolean = false; + public failedRunAway: boolean = false; + public joinedRound: boolean = false; } export enum AiType { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 578b9aba4fc..1aed61b34ba 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -244,7 +244,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } - const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ]; + const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]; if (lang && availableLangs.includes(lang)) { this.loadImage("halloween2024-event-" + lang, "events"); } else { diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index e68e9a06fae..dfa46ce3667 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -19,7 +19,7 @@ import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; +import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -121,18 +121,41 @@ export class ModifierType { * Populates item tier for ModifierType instance * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * It checks the weight of the item and will use the first tier for which the weight is greater than 0 + * This is to allow items to be in multiple item pools depending on the conditions, for example for events + * If all tiers have a weight of 0 for the item, the first tier where the item was found is used * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from + * @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) + * if not provided or empty, the weight check will be ignored + * @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) */ - withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { + withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER, party?: PlayerPokemon[], rerollCount: number = 0): ModifierType { + let defaultTier: undefined | ModifierTier; for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const modifier of tier) { if (this.id === modifier.modifierType.id) { - this.tier = modifier.modifierType.tier; - return this; + let weight: number; + if (modifier.weight instanceof Function) { + weight = party ? modifier.weight(party, rerollCount) : 0; + } else { + weight = modifier.weight; + } + if (weight > 0) { + this.tier = modifier.modifierType.tier; + return this; + } else if (isNullOrUndefined(defaultTier)) { + // If weight is 0, keep track of the first tier where the item was found + defaultTier = modifier.modifierType.tier; + } } } } + // Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any + if (defaultTier) { + this.tier = defaultTier; + } + return this; } @@ -2117,7 +2140,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo // Populates item id and tier guaranteedMod = guaranteedMod .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(); + .withTierFromPool(ModifierPoolType.PLAYER, party); const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; if (modType) { @@ -2186,7 +2209,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], } if (modifierType) { - options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(); + options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party); } } } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 36f94b99b20..0891262649c 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -746,7 +746,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { return 0; } if (pokemon.isPlayer() && forThreshold) { - return scene.getParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); + return scene.getPlayerParty().map(p => this.getMaxHeldItemCount(p)).reduce((stackCount: number, maxStackCount: number) => Math.max(stackCount, maxStackCount), 0); } return this.getMaxHeldItemCount(pokemon); } @@ -1767,10 +1767,10 @@ export class HitHealModifier extends PokemonHeldItemModifier { * @returns `true` if the {@linkcode Pokemon} was healed */ override apply(pokemon: Pokemon): boolean { - if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) { + if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) { const scene = pokemon.scene; scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), - toDmgValue(pokemon.turnData.damageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); + toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); } return true; @@ -2022,7 +2022,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier { abstract override apply(playerPokemon: PlayerPokemon, ...args: unknown[]): boolean | Promise; getPokemon(scene: BattleScene) { - return scene.getParty().find(p => p.id === this.pokemonId); + return scene.getPlayerParty().find(p => p.id === this.pokemonId); } } @@ -2224,7 +2224,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); - playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level)); + playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level)); return true; } @@ -2244,7 +2244,7 @@ export class TmModifier extends ConsumablePokemonModifier { */ override apply(playerPokemon: PlayerPokemon): boolean { - playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM)); + playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM)); return true; } @@ -2266,7 +2266,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier { */ override apply(playerPokemon: PlayerPokemon, cost?: number): boolean { - playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost)); + playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getPlayerParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost)); return true; } @@ -2783,7 +2783,7 @@ export class MoneyRewardModifier extends ConsumableModifier { battleScene.addMoney(moneyAmount.value); - battleScene.getParty().map(p => { + battleScene.getPlayerParty().map(p => { if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { p.evoCounter ? p.evoCounter++ : p.evoCounter = 1; const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier; diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 3e46fc792f0..483e6eac943 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -1,21 +1,22 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball"; +import BattleScene from "#app/battle-scene"; +import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { SubstituteTag } from "#app/data/battler-tags"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; -import { PokeballType } from "#app/enums/pokeball"; -import { StatusEffect } from "#app/enums/status-effect"; -import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; import { achvs } from "#app/system/achv"; -import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { Mode } from "#app/ui/ui"; +import { PokeballType } from "#enums/pokeball"; +import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; -import { PokemonPhase } from "./pokemon-phase"; -import { VictoryPhase } from "./victory-phase"; -import { SubstituteTag } from "#app/data/battler-tags"; export class AttemptCapturePhase extends PokemonPhase { private pokeballType: PokeballType; @@ -235,7 +236,7 @@ export class AttemptCapturePhase extends PokemonPhase { const addToParty = (slotIndex?: number) => { const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); - if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { + if (this.scene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) { this.scene.validateAchv(achvs.SHINY_PARTY); } Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => { @@ -249,7 +250,7 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { - if (this.scene.getParty().length === 6) { + if (this.scene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { const promptRelease = () => { this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index e0dd7fa72fd..b4768dc9a26 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -10,6 +10,10 @@ import { NewBattlePhase } from "./new-battle-phase"; import { PokemonPhase } from "./pokemon-phase"; export class AttemptRunPhase extends PokemonPhase { + + /** For testing purposes: this is to force the pokemon to fail and escape */ + public forceFailEscape = false; + constructor(scene: BattleScene, fieldIndex: number) { super(scene, fieldIndex); } @@ -28,7 +32,7 @@ export class AttemptRunPhase extends PokemonPhase { applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance); - if (playerPokemon.randSeedInt(100) < escapeChance.value) { + if (playerPokemon.randSeedInt(100) < escapeChance.value && !this.forceFailEscape) { this.scene.playSound("se/flee"); this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); @@ -51,6 +55,7 @@ export class AttemptRunPhase extends PokemonPhase { this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene)); } else { + playerPokemon.turnData.failedRunAway = true; this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500); } diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index bae61aa2288..3b9ca012ef7 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -1,8 +1,8 @@ +import BattleScene from "#app/battle-scene"; import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/ability"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; import { GameOverPhase } from "./game-over-phase"; -import BattleScene from "#app/battle-scene"; export class BattleEndPhase extends BattlePhase { /** If true, will increment battles won */ @@ -41,7 +41,7 @@ export class BattleEndPhase extends BattlePhase { } } - for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { + for (const pokemon of this.scene.getPokemonAllowedInBattle()) { applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); } diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 8849d304435..b87dff32f60 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -37,7 +37,7 @@ export class CheckSwitchPhase extends BattlePhase { return; } - if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) { + if (!this.scene.getPlayerParty().slice(1).filter(p => p.isActive()).length) { super.end(); return; } @@ -51,7 +51,7 @@ export class CheckSwitchPhase extends BattlePhase { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); - this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, false, true)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.INITIAL_SWITCH, this.fieldIndex, false, true)); this.end(); }, () => { this.scene.ui.setMode(Mode.MESSAGE); diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 6d4d46c51c9..c1d38815c2f 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -17,6 +17,8 @@ import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { isNullOrUndefined } from "#app/utils"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -228,32 +230,43 @@ export class CommandPhase extends FieldPhase { }, null, true); } else { const trapTag = playerPokemon.getTag(TrappedTag); + const fairyLockTag = playerPokemon.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); // trapTag should be defined at this point, but just in case... - if (!trapTag) { + if (!trapTag && !fairyLockTag) { currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; break; } - if (!isSwitch) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); } - this.scene.ui.showText( - i18next.t("battle:noEscapePokemon", { - pokemonName: trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)!) : "", - moveName: trapTag.getMoveName(), - escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") - }), - null, - () => { - this.scene.ui.showText("", 0); - if (!isSwitch) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - }, null, true); + const showNoEscapeText = (tag: any) => { + this.scene.ui.showText( + i18next.t("battle:noEscapePokemon", { + pokemonName: tag.sourceId && this.scene.getPokemonById(tag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(tag.sourceId)!) : "", + moveName: tag.getMoveName(), + escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") + }), + null, + () => { + this.scene.ui.showText("", 0); + if (!isSwitch) { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + } + }, + null, + true + ); + }; + + if (trapTag) { + showNoEscapeText(trapTag); + } else if (fairyLockTag) { + showNoEscapeText(fairyLockTag); + } } } break; diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index b7071c4cc6f..c4d919c0325 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -1,40 +1,40 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex, BattleType } from "#app/battle"; +import BattleScene from "#app/battle-scene"; +import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability"; +import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { TrainerSlot } from "#app/data/trainer-config"; import { getRandomWeatherType } from "#app/data/weather"; -import { BattleSpec } from "#app/enums/battle-spec"; -import { PlayerGender } from "#app/enums/player-gender"; -import { Species } from "#app/enums/species"; import { EncounterPhaseEvent } from "#app/events/battle-scene"; import Pokemon, { FieldPosition } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import Overrides from "#app/overrides"; +import { BattlePhase } from "#app/phases/battle-phase"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; import { achvs } from "#app/system/achv"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; -import { BattlePhase } from "./battle-phase"; -import * as Utils from "#app/utils"; -import { randSeedInt } from "#app/utils"; -import { CheckSwitchPhase } from "./check-switch-phase"; -import { GameOverPhase } from "./game-over-phase"; -import { PostSummonPhase } from "./post-summon-phase"; -import { ReturnPhase } from "./return-phase"; -import { ScanIvsPhase } from "./scan-ivs-phase"; -import { ShinySparklePhase } from "./shiny-sparkle-phase"; -import { SummonPhase } from "./summon-phase"; -import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; -import Overrides from "#app/overrides"; -import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; -import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; -import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { randSeedInt, randSeedItem } from "#app/utils"; +import { BattleSpec } from "#enums/battle-spec"; import { Biome } from "#enums/biome"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { PlayerGender } from "#enums/player-gender"; +import { Species } from "#enums/species"; +import i18next from "i18next"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; export class EncounterPhase extends BattlePhase { @@ -116,7 +116,7 @@ export class EncounterPhase extends BattlePhase { if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { battle.enemyParty[e].ivs = new Array(6).fill(31); } - this.scene.getParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => { + this.scene.getPlayerParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => { applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, false, battle.enemyParty[e]); }); } @@ -156,7 +156,7 @@ export class EncounterPhase extends BattlePhase { return true; }); - if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { + if (this.scene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) { this.scene.validateAchv(achvs.SHINY_PARTY); } @@ -248,7 +248,7 @@ export class EncounterPhase extends BattlePhase { /*if (startingWave > 10) { for (let m = 0; m < Math.min(Math.floor(startingWave / 10), 99); m++) - this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getParty())[0].type.newModifier(), true); + this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getPlayerParty())[0].type.newModifier(), true); this.scene.updateModifiers(true); }*/ @@ -259,7 +259,7 @@ export class EncounterPhase extends BattlePhase { this.scene.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS; } - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); } @@ -338,7 +338,7 @@ export class EncounterPhase extends BattlePhase { const doSummon = () => { this.scene.currentBattle.started = true; this.scene.playBgm(undefined); - this.scene.pbTray.showPbTray(this.scene.getParty()); + this.scene.pbTray.showPbTray(this.scene.getPlayerParty()); this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); const doTrainerSummon = () => { this.hideEnemyTrainer(); @@ -362,7 +362,7 @@ export class EncounterPhase extends BattlePhase { doSummon(); } else { let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); + this.scene.executeWithSeedOffset(() => message = randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); message = message!; // tell TS compiler it's defined now const showDialogueAndSummon = () => { this.scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => { @@ -447,13 +447,13 @@ export class EncounterPhase extends BattlePhase { if (![ BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER ].includes(this.scene.currentBattle.battleType)) { enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { // if there is not a player party, we can't continue - if (!this.scene.getParty()?.length) { + if (!this.scene.getPlayerParty().length) { return false; } // how many player pokemon are on the field ? - const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length; + const pokemonsOnFieldCount = this.scene.getPlayerParty().filter(p => p.isOnField()).length; // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); + const requiredPokemonsOnField = Math.min(this.scene.getPlayerParty().filter((p) => !p.isFainted()).length, 2); // if it's a double, there should be 2, otherwise 1 if (this.scene.currentBattle.double) { return pokemonsOnFieldCount === requiredPokemonsOnField; @@ -467,7 +467,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const availablePartyMembers = this.scene.getPokemonAllowedInBattle(); if (!availablePartyMembers[0].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 0)); diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 59b73fe9e11..7b079c3fbf9 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -223,7 +223,7 @@ export class EvolutionPhase extends Phase { this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); for (const lm of levelMoves) { - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getParty().indexOf(this.pokemon), lm[1])); + this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); } this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index eee1fd52938..3e90233a38c 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -1,24 +1,24 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex, BattleType } from "#app/battle"; -import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability"; +import BattleScene from "#app/battle-scene"; +import { applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostVictoryAbAttrs, PostFaintAbAttr, PostKnockOutAbAttr, PostVictoryAbAttr } from "#app/data/ability"; import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags"; import { battleSpecDialogue } from "#app/data/dialogue"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { BattleSpec } from "#app/enums/battle-spec"; import { StatusEffect } from "#app/enums/status-effect"; -import Pokemon, { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon"; +import Pokemon, { EnemyPokemon, HitResult, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { SwitchType } from "#enums/switch-type"; import i18next from "i18next"; import { DamagePhase } from "./damage-phase"; +import { GameOverPhase } from "./game-over-phase"; import { PokemonPhase } from "./pokemon-phase"; +import { SwitchPhase } from "./switch-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; -import { GameOverPhase } from "./game-over-phase"; -import { SwitchPhase } from "./switch-phase"; import { VictoryPhase } from "./victory-phase"; -import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; -import { SwitchType } from "#enums/switch-type"; import { isNullOrUndefined } from "#app/utils"; import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters"; @@ -65,6 +65,15 @@ export class FaintPhase extends PokemonPhase { } } + /** In case the current pokemon was just switched in, make sure it is counted as participating in the combat */ + this.scene.getPlayerField().forEach((pokemon, i) => { + if (pokemon?.isActive(true)) { + if (pokemon.isPlayer()) { + this.scene.currentBattle.addParticipant(pokemon as PlayerPokemon); + } + } + }); + if (!this.tryOverrideForBattleSpec()) { this.doFaint(); } @@ -111,7 +120,7 @@ export class FaintPhase extends PokemonPhase { if (this.player) { /** The total number of Pokemon in the player's party that can legally fight */ - const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const legalPlayerPokemon = this.scene.getPokemonAllowedInBattle(); /** The total number of legal player Pokemon that aren't currently on the field */ const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); if (!legalPlayerPokemon.length) { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 9c444fc40f0..1302d8fc652 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -1,8 +1,8 @@ import { clientSessionId } from "#app/account"; import { BattleType } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; +import { getCharVariantFromDialogue } from "#app/data/dialogue"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import { trainerConfigs } from "#app/data/trainer-config"; import Pokemon from "#app/field/pokemon"; @@ -65,7 +65,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.pushPhase(new EncounterPhase(this.scene, true)); - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; + const availablePartyMembers = this.scene.getPokemonAllowedInBattle().length; this.scene.pushPhase(new SummonPhase(this.scene, 0)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -97,7 +97,7 @@ export class GameOverPhase extends BattlePhase { firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY); this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); this.scene.gameData.gameStats.sessionsWon++; - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { this.awardRibbon(pokemon); if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) { @@ -195,13 +195,13 @@ export class GameOverPhase extends BattlePhase { if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE)); } - if (this.scene.getParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { + if (this.scene.getPlayerParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE)); } if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE)); } - if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) { + if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) { this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.EVIOLITE)); } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 2b898f7d66b..8b4b462380c 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,20 +1,62 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; +import BattleScene from "#app/battle-scene"; +import { + AddSecondStrikeAbAttr, + AlwaysHitAbAttr, + applyPostAttackAbAttrs, + applyPostDefendAbAttrs, + applyPreAttackAbAttrs, + IgnoreMoveEffectsAbAttr, + MaxMultiHitAbAttr, + PostAttackAbAttr, + PostDefendAbAttr, + TypeImmunityAbAttr, +} from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; -import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; -import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; +import { + BattlerTagLapseType, + DamageProtectedTag, + ProtectedTag, + SemiInvulnerableTag, + SubstituteTag, +} from "#app/data/battler-tags"; +import { + applyFilteredMoveAttrs, + applyMoveAttrs, + AttackMove, + FixedDamageAttr, + HitsTagAttr, + MissEffectAttr, + MoveAttr, + MoveCategory, + MoveEffectAttr, + MoveEffectTrigger, + MoveFlags, + MoveTarget, + MultiHitAttr, + NoEffectAttr, + OneHitKOAttr, + OverrideMoveEffectAttr, + ToxicAccuracyAttr, + VariableTargetAttr, +} from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { Moves } from "#app/enums/moves"; -import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier"; -import i18next from "i18next"; -import * as Utils from "#app/utils"; -import { PokemonPhase } from "./pokemon-phase"; import { Type } from "#app/data/type"; +import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { + ContactHeldItemTransferChanceModifier, + EnemyAttackStatusEffectChanceModifier, + FlinchChanceModifier, + HitHealModifier, + PokemonMultiHitModifier, +} from "#app/modifier/modifier"; +import { BooleanHolder, executeIf, NumberHolder } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import i18next from "i18next"; +import { PokemonPhase } from "./pokemon-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase { this.targets = targets; } - start() { + public override start(): void { super.start(); /** The Pokemon using this phase's invoked move */ @@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase { * Does an effect from this move override other effects on this turn? * e.g. Charging moves (Fly, etc.) on their first turn of use. */ - const overridden = new Utils.BooleanHolder(false); + const overridden = new BooleanHolder(false); /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ const move = this.move.getMove(); // Assume single target for override - applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => { // If other effects were overriden, stop this phase before they can be applied if (overridden.value) { return this.end(); @@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase { * effects of the move itself, Parental Bond, and Multi-Lens to do so. */ if (user.turnData.hitsLeft === -1) { - const hitCount = new Utils.IntegerHolder(1); + const hitCount = new NumberHolder(1); // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); + applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0)); // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); + this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0)); } // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; @@ -100,8 +142,9 @@ export class MoveEffectPhase extends PokemonPhase { const hasActiveTargets = targets.some(t => t.isActive(true)); /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ - const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !targets[0].getTag(SemiInvulnerableTag); + const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) + && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !targets[0].getTag(SemiInvulnerableTag); /** * If no targets are left for the move to hit (FAIL), or the invoked move is single-target @@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" })); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); moveHistoryEntry.result = MoveResult.MISS; applyMoveAttrs(MissEffectAttr, user, null, move); } else { @@ -127,30 +170,40 @@ export class MoveEffectPhase extends PokemonPhase { const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { + new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { + // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles + if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) { + continue; + } /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ - const hasConditionalProtectApplied = new Utils.BooleanHolder(false); + const hasConditionalProtectApplied = new BooleanHolder(false); /** Does the applied conditional protection bypass Protect-ignoring effects? */ - const bypassIgnoreProtect = new Utils.BooleanHolder(false); + const bypassIgnoreProtect = new BooleanHolder(false); /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ if (!this.move.getMove().isAllyTarget()) { this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) - && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) - || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + const isProtected = ( + bypassIgnoreProtect.value + || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) + && (hasConditionalProtectApplied.value + || (!target.findTags(t => t instanceof DamageProtectedTag).length + && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) + || (this.move.getMove().category !== MoveCategory.STATUS + && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ - const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !target.getTag(SemiInvulnerableTag); + const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) + && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !target.getTag(SemiInvulnerableTag); /** * If the move missed a target, stop all future hits against that target @@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Does this phase represent the invoked move's last strike? */ - const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); + const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()); /** * If the user can change forms by using the invoked move, @@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase { * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * type requires different conditions to be met with respect to the move's hit result. */ - applyAttrs.push(new Promise(resolve => { - // Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, - user, target, move).then(() => { - // All other effects require the move to not have failed or have been cancelled to trigger - if (hitResult !== HitResult.FAIL) { - /** - * If the invoked move's effects are meant to trigger during the move's "charge turn," - * ignore all effects after this point. - * Otherwise, apply all self-targeted POST_APPLY effects. - */ - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => { - // All effects past this point require the move to have hit the target - if (hitResult !== HitResult.NO_EFFECT) { - // Apply all non-self-targeted POST_APPLY effects - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { - /** - * If the move hit, and the target doesn't have Shield Dust, - * apply the chance to flinch the target gained from King's Rock - */ - if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { - const flinched = new Utils.BooleanHolder(false); - user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); - if (flinched.value) { - target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); - } - } - // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT - && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { - // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { - // Only apply the following effects if the move was not deflected by a substitute - if (move.hitsSubstitute(user, target)) { - return resolve(); - } + const k = new Promise((resolve) => { + //Start promise chain and apply PRE_APPLY move attributes + let promiseChain: Promise = applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.PRE_APPLY + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit) + && hitResult !== HitResult.NO_EFFECT, user, target, move); - // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens - if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { - user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); - } - target.lapseTags(BattlerTagLapseType.AFTER_HIT); + /** Don't complete if the move failed */ + if (hitResult === HitResult.FAIL) { + return resolve(); + } - })).then(() => { - // Apply the user's post-attack ability effects - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { - /** - * If the invoked move is an attack, apply the user's chance to - * steal an item from the target granted by Grip Claw - */ - if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); - } - resolve(); - }); - }); - }) - ).then(() => resolve()); - }); - } else { - applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); - } - }); - } else { - resolve(); - } - }); - })); + /** Apply Move/Ability Effects in correct order */ + promiseChain = promiseChain + .then(this.applySelfTargetEffects(user, target, firstHit, lastHit)); + + if (hitResult !== HitResult.NO_EFFECT) { + promiseChain + .then(this.applyPostApplyEffects(user, target, firstHit, lastHit)) + .then(this.applyHeldItemFlinchCheck(user, target, dealsDamage)) + .then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget)) + .then(() => resolve()); + } else { + promiseChain + .then(() => applyMoveAttrs(NoEffectAttr, user, null, move)) + .then(resolve); + } + }); + + applyAttrs.push(k); } + // 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.getTarget()?.isActive()) ? + const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : null; - if (!!postTarget) { + if (postTarget) { if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after - applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); + applyAttrs[applyAttrs.length - 1].then(() => postTarget); } else { // Otherwise, push a new asynchronous move effect applyAttrs.push(postTarget); } @@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase { */ targets.forEach(target => { const substitute = target.getTag(SubstituteTag); - if (!!substitute && substitute.hp <= 0) { + if (substitute && substitute.hp <= 0) { target.lapseTag(BattlerTagType.SUBSTITUTE); } }); @@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase { }); } - end() { + public override end(): void { const user = this.getUserPokemon(); /** * If this phase isn't for the invoked move's last strike, @@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase { * to the user. */ if (user) { - if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { + if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); } else { // Queue message for number of hits made by multi-move @@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Resolves whether this phase's invoked move hits or misses the given target - * @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move - * @returns `true` if the move does not miss the target; `false` otherwise - */ - hitCheck(target: Pokemon): boolean { + * Apply self-targeted effects that trigger `POST_APPLY` + * + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first 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. + */ + protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise { + return () => applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.POST_APPLY + && attr.selfTarget + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()); + } + + /** + * Applies non-self-targeted effects that trigger `POST_APPLY` + * (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first 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. + */ + protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise { + return () => applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.POST_APPLY + && !attr.selfTarget + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()); + } + + /** + * Applies effects that trigger on HIT + * (i.e. Final Gambit, Power-Up Punch, Drain Punch) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @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. + */ + protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise { + return applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.HIT + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit) + && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()); + } + + /** + * Applies reactive effects that occur when a Pokémon is hit. + * (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param hitResult - The {@linkcode HitResult} of the attempted move + * @returns a `Promise` intended to be passed into a `then()` call. + */ + protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise { + return executeIf(!target.isFainted() || target.canApplyAbility(), () => + applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult) + .then(() => { + + if (!this.move.getMove().hitsSubstitute(user, target)) { + if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { + user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); + } + + target.lapseTags(BattlerTagLapseType.AFTER_HIT); + } + + }) + ); + } + + /** + * Applies all effects and attributes that require a move to connect with a target, + * namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @param isProtected - `true` if the target is protected by effects such as Protect + * @param hitResult - The {@linkcode HitResult} of the attempted 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. + */ + protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise { + return () => executeIf(!isProtected, () => + this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() => + this.applyOnGetHitAbEffects(user, target, hitResult)).then(() => + applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects + + if (this.move.getMove() instanceof AttackMove) { + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + } + }) + ); + } + + /** + * Handles checking for and applying Flinches + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param dealsDamage - `true` if the attempted move successfully dealt damage + * @returns a function intended to be passed into a `then()` call. + */ + protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { + return () => { + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { + const flinched = new BooleanHolder(false); + user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + if (flinched.value) { + target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); + } + } + }; + } + + /** + * Resolves whether this phase's invoked move hits the given target + * @param target - The {@linkcode Pokemon} targeted by the invoked move + * @returns `true` if the move hits the target + */ + public hitCheck(target: Pokemon): boolean { // Moves targeting the user and entry hazards can't miss if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) { return true; @@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase { return rand < (moveAccuracy * accuracyMultiplier); } - /** Returns the {@linkcode Pokemon} using this phase's invoked move */ - getUserPokemon(): Pokemon | undefined { + /** @returns The {@linkcode Pokemon} using this phase's invoked move */ + public getUserPokemon(): Pokemon | undefined { if (this.battlerIndex > BattlerIndex.ENEMY_2) { return this.scene.getPokemonById(this.battlerIndex) ?? undefined; } return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; } - /** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ - getTargets(): Pokemon[] { + /** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */ + public getTargets(): Pokemon[] { return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); } - /** Returns the first target of this phase's invoked move */ - getTarget(): Pokemon | undefined { + /** @returns The first target of this phase's invoked move */ + public getFirstTarget(): Pokemon | undefined { return this.getTargets()[0]; } /** * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed + * @param target - The {@linkcode Pokemon} to be removed */ - removeTarget(target: Pokemon): void { + protected removeTarget(target: Pokemon): void { const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); if (targetIndex !== -1) { this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); @@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase { * @param target {@linkcode Pokemon} if defined, only stop subsequent * strikes against this Pokemon */ - stopMultiHit(target?: Pokemon): void { - /** If given a specific target, remove the target from subsequent strikes */ + public stopMultiHit(target?: Pokemon): void { + // If given a specific target, remove the target from subsequent strikes if (target) { this.removeTarget(target); } - /** - * If no target specified, or the specified target was the last of this move's - * targets, completely cancel all subsequent strikes. - */ + const user = this.getUserPokemon(); + if (!user) { + return; + } + // If no target specified, or the specified target was the last of this move's + // targets, completely cancel all subsequent strikes. if (!target || this.targets.length === 0 ) { - this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? - this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; } } - /** Returns a new MoveEffectPhase with the same properties as this phase */ - getNewHitPhase() { + /** @returns A new `MoveEffectPhase` with the same properties as this phase */ + protected getNewHitPhase(): MoveEffectPhase { return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index a516fd8593d..5c6c339ffa5 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,7 +3,7 @@ import BattleScene from "#app/battle-scene"; import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; -import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; +import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, frenzyMissFunc, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { Type } from "#app/data/type"; @@ -470,6 +470,10 @@ export class MovePhase extends BattlePhase { this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); } + if (this.cancelled && this.pokemon.summonData?.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) { + frenzyMissFunc(this.pokemon, this.move.getMove()); + } + this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 49e78fa5369..e7d1f7e9074 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -1,31 +1,31 @@ +import { BattlerTagLapseType } from "#app/data/battler-tags"; +import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; +import { BattleSpec } from "#enums/battle-spec"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { SwitchType } from "#enums/switch-type"; import i18next from "i18next"; import BattleScene from "../battle-scene"; +import { getCharVariantFromDialogue } from "../data/dialogue"; +import { OptionSelectSettings, transitionMysteryEncounterIntroVisuals } from "../data/mystery-encounters/utils/encounter-phase-utils"; +import { TrainerSlot } from "../data/trainer-config"; +import { IvScannerModifier } from "../modifier/modifier"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; -import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; -import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { getCharVariantFromDialogue } from "../data/dialogue"; -import { TrainerSlot } from "../data/trainer-config"; -import { BattleSpec } from "#enums/battle-spec"; -import { IvScannerModifier } from "../modifier/modifier"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; -import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { BattlerTagLapseType } from "#app/data/battler-tags"; -import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; -import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; -import { SummonPhase } from "#app/phases/summon-phase"; -import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; -import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; -import { ReturnPhase } from "#app/phases/return-phase"; -import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; -import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; -import { NewBattlePhase } from "#app/phases/new-battle-phase"; -import { GameOverPhase } from "#app/phases/game-over-phase"; -import { SwitchPhase } from "#app/phases/switch-phase"; -import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; -import { SwitchType } from "#enums/switch-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; /** * Will handle (in order): @@ -238,7 +238,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { } // The total number of Pokemon in the player's party that can legally fight - const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const legalPlayerPokemon = this.scene.getPokemonAllowedInBattle(); // The total number of legal player Pokemon that aren't currently on the field const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); if (!legalPlayerPokemon.length) { @@ -343,7 +343,7 @@ export class MysteryEncounterBattlePhase extends Phase { const doSummon = () => { scene.currentBattle.started = true; scene.playBgm(undefined); - scene.pbTray.showPbTray(scene.getParty()); + scene.pbTray.showPbTray(scene.getPlayerParty()); scene.pbTrayEnemy.showPbTray(scene.getEnemyParty()); const doTrainerSummon = () => { this.hideEnemyTrainer(); @@ -402,7 +402,7 @@ export class MysteryEncounterBattlePhase extends Phase { } } - const availablePartyMembers = scene.getParty().filter(p => p.isAllowedInBattle()); + const availablePartyMembers = scene.getPlayerParty().filter(p => p.isAllowedInBattle()); if (!availablePartyMembers[0].isOnField()) { scene.pushPhase(new SummonPhase(scene, 0)); diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index eea591c3936..910306b76ad 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -11,13 +11,13 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { doEncounter(): void { this.scene.playBgm(undefined, true); - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); } } - for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) { + for (const pokemon of this.scene.getPlayerParty().filter(p => p.isOnField())) { applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); } diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index 407d7c26b5d..e086ed4fe3e 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -13,7 +13,7 @@ export class NextEncounterPhase extends EncounterPhase { doEncounter(): void { this.scene.playBgm(undefined, true); - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { if (pokemon) { pokemon.resetBattleData(); } diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index e6ee11202df..4841bf9a5b4 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -19,7 +19,7 @@ export class PartyHealPhase extends BattlePhase { this.scene.fadeOutBgm(1000, false); } this.scene.ui.fadeOut(1000).then(() => { - for (const pokemon of this.scene.getParty()) { + for (const pokemon of this.scene.getPlayerParty()) { pokemon.hp = pokemon.getMaxHp(); pokemon.resetStatus(); for (const move of pokemon.moveset) { diff --git a/src/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts index 2b6ca01261d..f2e2b23bfb2 100644 --- a/src/phases/party-member-pokemon-phase.ts +++ b/src/phases/party-member-pokemon-phase.ts @@ -18,7 +18,7 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase { } getParty(): Pokemon[] { - return this.player ? this.scene.getParty() : this.scene.getEnemyParty(); + return this.player ? this.scene.getPlayerParty() : this.scene.getEnemyParty(); } getPokemon(): Pokemon { diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 2efd992a2b5..08e4d7cb952 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; import { getStatusEffectActivationText } from "#app/data/status-effect"; import { BattleSpec } from "#app/enums/battle-spec"; @@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); + applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); } else { diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index e5a60692bb4..98975e30720 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -38,7 +38,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.reroll = false; } - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); if (!this.isCopy) { regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); } @@ -289,7 +289,7 @@ export class SelectModifierPhase extends BattlePhase { } getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] { - return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); + return getPlayerModifierTypeOptions(modifierCount, this.scene.getPlayerParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); } copy(): SelectModifierPhase { diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 1692b5f2234..2273ab1cd3c 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -3,16 +3,15 @@ import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { Gender } from "#app/data/gender"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import { Species } from "#app/enums/species"; -import { PlayerPokemon } from "#app/field/pokemon"; -import { overrideModifiers, overrideHeldItems } from "#app/modifier/modifier"; +import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; +import Overrides from "#app/overrides"; import { Phase } from "#app/phase"; +import { TitlePhase } from "#app/phases/title-phase"; import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler"; import { Starter } from "#app/ui/starter-select-ui-handler"; import { Mode } from "#app/ui/ui"; +import { Species } from "#enums/species"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { TitlePhase } from "./title-phase"; -import Overrides from "#app/overrides"; export class SelectStarterPhase extends Phase { @@ -44,7 +43,7 @@ export class SelectStarterPhase extends Phase { * @param starters {@linkcode Pokemon} with which to start the first battle */ initBattle(starters: Starter[]) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; starters.forEach((starter: Starter, i: integer) => { if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { @@ -103,7 +102,7 @@ export class SelectStarterPhase extends Phase { this.scene.sessionPlayTime = 0; this.scene.lastSavePlayTime = 0; // Ensures Keldeo (or any future Pokemon that have this type of form change) starts in the correct form - this.scene.getParty().forEach((p: PlayerPokemon) => { + this.scene.getPlayerParty().forEach((p) => { this.scene.triggerPokemonFormChange(p, SpeciesFormChangeMoveLearnedTrigger); }); this.end(); diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index f5ce2179715..2abb109a529 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -1,7 +1,7 @@ import BattleScene from "#app/battle-scene"; -import { SwitchType } from "#enums/switch-type"; -import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; +import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; +import { SwitchType } from "#enums/switch-type"; import { BattlePhase } from "./battle-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; @@ -38,7 +38,7 @@ export class SwitchPhase extends BattlePhase { super.start(); // Skip modal switch if impossible (no remaining party members that aren't in battle) - if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { + if (this.isModal && !this.scene.getPlayerParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { return super.end(); } @@ -49,7 +49,7 @@ export class SwitchPhase extends BattlePhase { * if the mon should have already been returned but is still alive and well * on the field. see also; battle.test.ts */ - if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) { + if (this.isModal && !this.doReturn && !this.scene.getPlayerParty()[this.fieldIndex].isFainted()) { return super.end(); } @@ -59,7 +59,7 @@ export class SwitchPhase extends BattlePhase { } // Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once - const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0; + const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getPokemonAllowedInBattle().length > 1 ? this.fieldIndex : 0; this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 37652b3cfa4..36db8b7a7e7 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -54,7 +54,7 @@ export class SwitchSummonPhase extends SummonPhase { } } - if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getParty() : this.scene.getEnemyParty())[this.slotIndex])) { + if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getPlayerParty() : this.scene.getEnemyParty())[this.slotIndex])) { if (this.player) { return this.switchAndSummon(); } else { @@ -64,10 +64,8 @@ export class SwitchSummonPhase extends SummonPhase { } const pokemon = this.getPokemon(); - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); - - if (this.switchType === SwitchType.SWITCH) { + if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { const substitute = pokemon.getTag(SubstituteTag); if (substitute) { this.scene.tweens.add({ @@ -186,6 +184,11 @@ export class SwitchSummonPhase extends SummonPhase { } } + if (this.switchType !== SwitchType.INITIAL_SWITCH) { + pokemon.resetTurnData(); + pokemon.turnData.switchedInThisTurn = true; + } + this.lastPokemon?.resetSummonData(); this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 58683cf8ec8..8338d39b81f 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -1,21 +1,21 @@ import { loggedInUser } from "#app/account"; -import BattleScene from "#app/battle-scene"; import { BattleType } from "#app/battle"; -import { getDailyRunStarters, fetchDailyRunSeed } from "#app/data/daily-run"; +import BattleScene from "#app/battle-scene"; +import { fetchDailyRunSeed, getDailyRunStarters } from "#app/data/daily-run"; import { Gender } from "#app/data/gender"; import { getBiomeKey } from "#app/field/arena"; -import { GameModes, GameMode, getGameMode } from "#app/game-mode"; -import { regenerateModifierPoolThresholds, ModifierPoolType, modifierTypes, getDailyRunStarterModifiers } from "#app/modifier/modifier-type"; +import { GameMode, GameModes, getGameMode } from "#app/game-mode"; +import { Modifier } from "#app/modifier/modifier"; +import { getDailyRunStarterModifiers, ModifierPoolType, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { Phase } from "#app/phase"; import { SessionSaveData } from "#app/system/game-data"; import { Unlockables } from "#app/system/unlockables"; import { vouchers } from "#app/system/voucher"; -import { OptionSelectItem, OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; +import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { SaveSlotUiMode } from "#app/ui/save-slot-select-ui-handler"; import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; import * as Utils from "#app/utils"; -import { Modifier } from "#app/modifier/modifier"; +import i18next from "i18next"; import { CheckSwitchPhase } from "./check-switch-phase"; import { EncounterPhase } from "./encounter-phase"; import { SelectChallengePhase } from "./select-challenge-phase"; @@ -203,7 +203,7 @@ export class TitlePhase extends Phase { const starters = getDailyRunStarters(this.scene, seed); const startingLevel = this.scene.gameMode.getStartingLevel(); - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; for (const starter of starters) { const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); @@ -276,7 +276,7 @@ export class TitlePhase extends Phase { this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded)); if (this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; + const availablePartyMembers = this.scene.getPokemonAllowedInBattle().length; this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { diff --git a/src/phases/toggle-double-position-phase.ts b/src/phases/toggle-double-position-phase.ts index 563af8575d7..eff92bc6acd 100644 --- a/src/phases/toggle-double-position-phase.ts +++ b/src/phases/toggle-double-position-phase.ts @@ -16,9 +16,9 @@ export class ToggleDoublePositionPhase extends BattlePhase { const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true)); if (playerPokemon) { - playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { + playerPokemon.setFieldPosition(this.double && this.scene.getPokemonAllowedInBattle().length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { if (playerPokemon.getFieldIndex() === 1) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); party[1] = party[0]; party[0] = playerPokemon; } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index 2f1b539cdcf..baff6c7d73f 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -1,15 +1,15 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; +import BattleScene from "#app/battle-scene"; +import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TurnInitEvent } from "#app/events/battle-scene"; import { PlayerPokemon } from "#app/field/pokemon"; import i18next from "i18next"; -import { FieldPhase } from "./field-phase"; -import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; import { CommandPhase } from "./command-phase"; import { EnemyCommandPhase } from "./enemy-command-phase"; +import { FieldPhase } from "./field-phase"; import { GameOverPhase } from "./game-over-phase"; +import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; import { TurnStartPhase } from "./turn-start-phase"; -import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -24,7 +24,7 @@ export class TurnInitPhase extends FieldPhase { if (p.isOnField() && !p.isAllowedInBattle()) { this.scene.queueMessage(i18next.t("challenges:illegalEvolution", { "pokemon": p.name }), null, true); - const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const allowedPokemon = this.scene.getPokemonAllowedInBattle(); if (!allowedPokemon.length) { // If there are no longer any legal pokemon in the party, game over. diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 91a67b9414c..845739dfcac 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -153,7 +153,7 @@ export async function initI18n(): Promise { i18next.use(new KoreanPostpositionProcessor()); await i18next.init({ fallbackLng: "en", - supportedLngs: [ "en", "es", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca-ES" ], + supportedLngs: [ "en", "es-ES", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca-ES" ], backend: { loadPath(lng: string, [ ns ]: string[]) { let fileName: string; diff --git a/src/system/achv.ts b/src/system/achv.ts index 366813328e2..d94fcba48f2 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -328,7 +328,7 @@ export const achvs = { HIDDEN_ABILITY: new Achv("HIDDEN_ABILITY", "", "HIDDEN_ABILITY.description", "ability_charm", 75), PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100), CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY", "", "CLASSIC_VICTORY.description", "relic_crown", 150, c => c.gameData.gameStats.sessionsWon === 0), - UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)), + UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, c => c.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)), MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 1 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 2 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, (c, scene) => c instanceof SingleGenerationChallenge && c.value === 3 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), diff --git a/src/system/game-data.ts b/src/system/game-data.ts index c00159a7fd7..e252e03afaf 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -949,7 +949,7 @@ export class GameData { seed: scene.seed, playTime: scene.sessionPlayTime, gameMode: scene.gameMode.modeId, - party: scene.getParty().map(p => new PokemonData(p)), + party: scene.getPlayerParty().map(p => new PokemonData(p)), enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)), modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), @@ -1028,7 +1028,7 @@ export class GameData { const loadPokemonAssets: Promise[] = []; - const party = scene.getParty(); + const party = scene.getPlayerParty(); party.splice(0, party.length); for (const p of sessionData.party) { @@ -1829,17 +1829,40 @@ export class GameData { return starterCount; } - getSpeciesDefaultDexAttr(species: PokemonSpecies, forSeen: boolean = false, optimistic: boolean = false): bigint { + getSpeciesDefaultDexAttr(species: PokemonSpecies, _forSeen: boolean = false, optimistic: boolean = false): bigint { let ret = 0n; const dexEntry = this.dexData[species.speciesId]; const attr = dexEntry.caughtAttr; - ret |= optimistic - ? attr & DexAttr.SHINY ? DexAttr.SHINY : DexAttr.NON_SHINY - : attr & DexAttr.NON_SHINY || !(attr & DexAttr.SHINY) ? DexAttr.NON_SHINY : DexAttr.SHINY; + if (optimistic) { + if (attr & DexAttr.SHINY) { + ret |= DexAttr.SHINY; + + if (attr & DexAttr.VARIANT_3) { + ret |= DexAttr.VARIANT_3; + } else if (attr & DexAttr.VARIANT_2) { + ret |= DexAttr.VARIANT_2; + } else { + ret |= DexAttr.DEFAULT_VARIANT; + } + } else { + ret |= DexAttr.NON_SHINY; + ret |= DexAttr.DEFAULT_VARIANT; + } + } else { + // Default to non shiny. Fallback to shiny if it's the only thing that's unlocked + ret |= (attr & DexAttr.NON_SHINY || !(attr & DexAttr.SHINY)) ? DexAttr.NON_SHINY : DexAttr.SHINY; + + if (attr & DexAttr.DEFAULT_VARIANT) { + ret |= DexAttr.DEFAULT_VARIANT; + } else if (attr & DexAttr.VARIANT_2) { + ret |= DexAttr.VARIANT_2; + } else if (attr & DexAttr.VARIANT_3) { + ret |= DexAttr.VARIANT_3; + } else { + ret |= DexAttr.DEFAULT_VARIANT; + } + } ret |= attr & DexAttr.MALE || !(attr & DexAttr.FEMALE) ? DexAttr.MALE : DexAttr.FEMALE; - ret |= optimistic - ? attr & DexAttr.SHINY ? attr & DexAttr.VARIANT_3 ? DexAttr.VARIANT_3 : attr & DexAttr.VARIANT_2 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT : DexAttr.DEFAULT_VARIANT - : attr & DexAttr.DEFAULT_VARIANT ? DexAttr.DEFAULT_VARIANT : attr & DexAttr.VARIANT_2 ? DexAttr.VARIANT_2 : attr & DexAttr.VARIANT_3 ? DexAttr.VARIANT_3 : DexAttr.DEFAULT_VARIANT; ret |= this.getFormAttr(this.getFormIndex(attr)); return ret; } @@ -1847,7 +1870,14 @@ export class GameData { getSpeciesDexAttrProps(species: PokemonSpecies, dexAttr: bigint): DexAttrProps { const shiny = !(dexAttr & DexAttr.NON_SHINY); const female = !(dexAttr & DexAttr.MALE); - const variant = dexAttr & DexAttr.DEFAULT_VARIANT ? 0 : dexAttr & DexAttr.VARIANT_2 ? 1 : dexAttr & DexAttr.VARIANT_3 ? 2 : 0; + let variant: Variant = 0; + if (dexAttr & DexAttr.DEFAULT_VARIANT) { + variant = 0; + } else if (dexAttr & DexAttr.VARIANT_2) { + variant = 1; + } else if (dexAttr & DexAttr.VARIANT_3) { + variant = 2; + } const formIndex = this.getFormIndex(dexAttr); return { diff --git a/src/system/modifier-data.ts b/src/system/modifier-data.ts index 1514f7e3fb3..c68f9ccb47d 100644 --- a/src/system/modifier-data.ts +++ b/src/system/modifier-data.ts @@ -38,7 +38,7 @@ export default class ModifierData { type.id = this.typeId; if (type instanceof ModifierTypeGenerator) { - type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyField(), this.typePregenArgs); + type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getPlayerParty() : scene.getEnemyField(), this.typePregenArgs); } const ret = Reflect.construct(constructor, ([ type ] as any[]).concat(this.args).concat(this.stackCount)) as PersistentModifier; diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index be440d5d93e..e6fb884ffdf 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -866,8 +866,8 @@ export function setSetting(scene: BattleScene, setting: string, value: integer): handler: () => changeLocaleHandler("en") }, { - label: "Español", - handler: () => changeLocaleHandler("es") + label: "Español (ES)", + handler: () => changeLocaleHandler("es-ES") }, { label: "Italiano", diff --git a/src/test/abilities/aroma_veil.test.ts b/src/test/abilities/aroma_veil.test.ts index 4284eb43a75..e74d0ff5a39 100644 --- a/src/test/abilities/aroma_veil.test.ts +++ b/src/test/abilities/aroma_veil.test.ts @@ -36,7 +36,7 @@ describe("Moves - Aroma Veil", () => { it("Aroma Veil protects the Pokemon's side against most Move Restriction Battler Tags", async () => { await game.classicMode.startBattle([ Species.REGIELEKI, Species.BULBASAUR ]); - const party = game.scene.getParty()! as PlayerPokemon[]; + const party = game.scene.getPlayerParty()! as PlayerPokemon[]; game.move.select(Moves.GROWL); game.move.select(Moves.GROWL); @@ -50,7 +50,7 @@ describe("Moves - Aroma Veil", () => { it("Aroma Veil does not protect against Imprison", async () => { await game.classicMode.startBattle([ Species.REGIELEKI, Species.BULBASAUR ]); - const party = game.scene.getParty()! as PlayerPokemon[]; + const party = game.scene.getPlayerParty()! as PlayerPokemon[]; game.move.select(Moves.GROWL); game.move.select(Moves.GROWL, 1); diff --git a/src/test/abilities/battle_bond.test.ts b/src/test/abilities/battle_bond.test.ts index 283fb0d0f14..e9970e1c049 100644 --- a/src/test/abilities/battle_bond.test.ts +++ b/src/test/abilities/battle_bond.test.ts @@ -40,7 +40,7 @@ describe("Abilities - BATTLE BOND", () => { it("check if fainted pokemon switches to base form on arena reset", async () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.GRENINJA ]); - const greninja = game.scene.getParty()[1]; + const greninja = game.scene.getPlayerParty()[1]; expect(greninja.formIndex).toBe(ashForm); greninja.hp = 0; diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index 0241aa4b9ea..f44782207cb 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -138,7 +138,7 @@ describe("Abilities - Disguise", () => { }); await game.classicMode.startBattle([ Species.FURRET, Species.MIMIKYU ]); - const mimikyu = game.scene.getParty()[1]!; + const mimikyu = game.scene.getPlayerParty()[1]!; expect(mimikyu.formIndex).toBe(bustedForm); game.move.select(Moves.SPLASH); diff --git a/src/test/abilities/forecast.test.ts b/src/test/abilities/forecast.test.ts index 6da31307789..18d43a67a9d 100644 --- a/src/test/abilities/forecast.test.ts +++ b/src/test/abilities/forecast.test.ts @@ -81,7 +81,7 @@ describe("Abilities - Forecast", () => { }); await game.startBattle([ Species.CASTFORM, Species.FEEBAS, Species.KYOGRE, Species.GROUDON, Species.RAYQUAZA, Species.ALTARIA ]); - vi.spyOn(game.scene.getParty()[5], "getAbility").mockReturnValue(allAbilities[Abilities.CLOUD_NINE]); + vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[Abilities.CLOUD_NINE]); const castform = game.scene.getPlayerField()[0]; expect(castform.formIndex).toBe(NORMAL_FORM); diff --git a/src/test/abilities/ice_face.test.ts b/src/test/abilities/ice_face.test.ts index 1c7f7bd6093..e31bee1c721 100644 --- a/src/test/abilities/ice_face.test.ts +++ b/src/test/abilities/ice_face.test.ts @@ -192,7 +192,7 @@ describe("Abilities - Ice Face", () => { game.doSwitchPokemon(1); await game.phaseInterceptor.to(TurnEndPhase); - eiscue = game.scene.getParty()[1]; + eiscue = game.scene.getPlayerParty()[1]; expect(eiscue.formIndex).toBe(noiceForm); expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBeUndefined(); diff --git a/src/test/abilities/mimicry.test.ts b/src/test/abilities/mimicry.test.ts new file mode 100644 index 00000000000..8f22de33061 --- /dev/null +++ b/src/test/abilities/mimicry.test.ts @@ -0,0 +1,91 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Mimicry", () => { + 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 ]) + .ability(Abilities.MIMICRY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(Moves.SPLASH); + }); + + it("Mimicry activates after the Pokémon with Mimicry is switched in while terrain is present, or whenever there is a change in terrain", async () => { + game.override.enemyAbility(Abilities.MISTY_SURGE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.ABRA ]); + + const [ playerPokemon1, playerPokemon2 ] = game.scene.getPlayerParty(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon1.getTypes().includes(Type.FAIRY)).toBe(true); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + expect(playerPokemon2.getTypes().includes(Type.FAIRY)).toBe(true); + }); + + it("Pokemon should revert back to its original, root type once terrain ends", async () => { + game.override + .moveset([ Moves.SPLASH, Moves.TRANSFORM ]) + .enemyAbility(Abilities.MIMICRY) + .enemyMoveset([ Moves.SPLASH, Moves.PSYCHIC_TERRAIN ]); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.TRANSFORM); + await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN); + await game.toNextTurn(); + expect(playerPokemon?.getTypes().includes(Type.PSYCHIC)).toBe(true); + + if (game.scene.arena.terrain) { + game.scene.arena.terrain.turnsLeft = 1; + } + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon?.getTypes().includes(Type.ELECTRIC)).toBe(true); + }); + + it("If the Pokemon is under the effect of a type-adding move and an equivalent terrain activates, the move's effect disappears", async () => { + game.override + .enemyMoveset([ Moves.FORESTS_CURSE, Moves.GRASSY_TERRAIN ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FORESTS_CURSE); + await game.toNextTurn(); + + expect(playerPokemon?.summonData.addedType).toBe(Type.GRASS); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.GRASSY_TERRAIN); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(playerPokemon?.summonData.addedType).toBeNull(); + expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true); + }); +}); diff --git a/src/test/abilities/pastel_veil.test.ts b/src/test/abilities/pastel_veil.test.ts index 660ff2616e9..6f09fd8ff06 100644 --- a/src/test/abilities/pastel_veil.test.ts +++ b/src/test/abilities/pastel_veil.test.ts @@ -51,7 +51,7 @@ describe("Abilities - Pastel Veil", () => { it("it heals the poisoned status condition of allies if user is sent out into battle", async () => { await game.startBattle([ Species.MAGIKARP, Species.FEEBAS, Species.GALAR_PONYTA ]); - const ponyta = game.scene.getParty()[2]; + const ponyta = game.scene.getPlayerParty()[2]; const magikarp = game.scene.getPlayerField()[0]; ponyta.abilityIndex = 1; diff --git a/src/test/abilities/power_construct.test.ts b/src/test/abilities/power_construct.test.ts index 1a9e7d4818a..bb80e9f3ac8 100644 --- a/src/test/abilities/power_construct.test.ts +++ b/src/test/abilities/power_construct.test.ts @@ -43,7 +43,7 @@ describe("Abilities - POWER CONSTRUCT", () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); - const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); + const zygarde = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.ZYGARDE); expect(zygarde).not.toBe(undefined); expect(zygarde!.formIndex).toBe(completeForm); @@ -73,7 +73,7 @@ describe("Abilities - POWER CONSTRUCT", () => { await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]); - const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE); + const zygarde = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.ZYGARDE); expect(zygarde).not.toBe(undefined); expect(zygarde!.formIndex).toBe(completeForm); diff --git a/src/test/abilities/schooling.test.ts b/src/test/abilities/schooling.test.ts index aacc7bbd4c2..5f953dbf9ab 100644 --- a/src/test/abilities/schooling.test.ts +++ b/src/test/abilities/schooling.test.ts @@ -43,7 +43,7 @@ describe("Abilities - SCHOOLING", () => { await game.startBattle([ Species.MAGIKARP, Species.WISHIWASHI ]); - const wishiwashi = game.scene.getParty().find((p) => p.species.speciesId === Species.WISHIWASHI)!; + const wishiwashi = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.WISHIWASHI)!; expect(wishiwashi).not.toBe(undefined); expect(wishiwashi.formIndex).toBe(schoolForm); diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index 3155594c81d..3318c7fc27a 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -43,7 +43,7 @@ describe("Abilities - Serene Grace", () => { game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); game.move.select(moveToUse); @@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => { const chance = new Utils.IntegerHolder(move.chance); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(30); }, 20000); @@ -70,7 +70,7 @@ describe("Abilities - Serene Grace", () => { ]); game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + expect(game.scene.getPlayerParty()[0].formIndex).toBe(0); game.move.select(moveToUse); @@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(60); }, 20000); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index a2600476d6d..826694752b7 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -1,17 +1,16 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; -import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; +import { NumberHolder } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { allMoves } from "#app/data/move"; - describe("Abilities - Sheer Force", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -39,13 +38,10 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force", async () => { const moveToUse = Moves.AIR_SLASH; game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -57,11 +53,11 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.AIR_SLASH); //Verify the move is boosted and has no chance of secondary effects - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(0); expect(power.value).toBe(move.power * 5461 / 4096); @@ -72,13 +68,11 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force with exceptions including binding moves", async () => { const moveToUse = Moves.BIND; game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -90,11 +84,11 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.BIND); //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -105,13 +99,11 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force with moves with no secondary effect", async () => { const moveToUse = Moves.TACKLE; game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -123,11 +115,11 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.TACKLE); //Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1 - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -140,13 +132,11 @@ describe("Abilities - Sheer Force", () => { game.override.enemyAbility(Abilities.COLOR_CHANGE); game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]); game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ - Species.PIDGEOT - ]); + await game.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.DEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); game.move.select(moveToUse); @@ -158,10 +148,10 @@ describe("Abilities - Sheer Force", () => { expect(move.id).toBe(Moves.CRUSH_CLAW); //Disable color change due to being hit by Sheer Force - const power = new Utils.IntegerHolder(move.power); - const chance = new Utils.IntegerHolder(move.chance); + const power = new NumberHolder(move.power); + const chance = new NumberHolder(move.chance); const user = phase.getUserPokemon()!; - const target = phase.getTarget()!; + const target = phase.getFirstTarget()!; const opponentType = target.getTypes()[0]; applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); @@ -186,7 +176,7 @@ describe("Abilities - Sheer Force", () => { Species.PIDGEOT ]); - const pidgeot = game.scene.getParty()[0]; + const pidgeot = game.scene.getPlayerParty()[0]; const onix = game.scene.getEnemyParty()[0]; pidgeot.stats[Stat.DEF] = 10000; diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 0f831fcf3fa..9f1e6aeb11d 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -1,11 +1,11 @@ import { BattlerIndex } from "#app/battle"; import { applyAbAttrs, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; -import { Stat } from "#enums/stat"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import * as Utils from "#app/utils"; +import { NumberHolder } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -27,26 +27,22 @@ describe("Abilities - Shield Dust", () => { beforeEach(() => { game = new GameManager(phaserGame); - const movesToUse = [ Moves.AIR_SLASH ]; game.override.battleType("single"); game.override.enemySpecies(Species.ONIX); game.override.enemyAbility(Abilities.SHIELD_DUST); game.override.startingLevel(100); - game.override.moveset(movesToUse); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override.moveset(Moves.AIR_SLASH); + game.override.enemyMoveset(Moves.TACKLE); }); it("Shield Dust", async () => { - const moveToUse = Moves.AIR_SLASH; - await game.startBattle([ - Species.PIDGEOT - ]); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; - expect(game.scene.getParty()[0].formIndex).toBe(0); + game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; + expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); - game.move.select(moveToUse); + game.move.select(Moves.AIR_SLASH); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.phaseInterceptor.to(MoveEffectPhase, false); @@ -56,9 +52,9 @@ describe("Abilities - Shield Dust", () => { const move = phase.move.getMove(); expect(move.id).toBe(Moves.AIR_SLASH); - const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); + const chance = new NumberHolder(move.chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); expect(chance.value).toBe(0); }, 20000); diff --git a/src/test/abilities/shields_down.test.ts b/src/test/abilities/shields_down.test.ts index d3d72ac80a5..fbb2e96e463 100644 --- a/src/test/abilities/shields_down.test.ts +++ b/src/test/abilities/shields_down.test.ts @@ -43,7 +43,7 @@ describe("Abilities - SHIELDS DOWN", () => { await game.startBattle([ Species.MAGIKARP, Species.MINIOR ]); - const minior = game.scene.getParty().find((p) => p.species.speciesId === Species.MINIOR)!; + const minior = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.MINIOR)!; expect(minior).not.toBe(undefined); expect(minior.formIndex).toBe(coreForm); diff --git a/src/test/abilities/speed_boost.test.ts b/src/test/abilities/speed_boost.test.ts new file mode 100644 index 00000000000..dd2e83aaa88 --- /dev/null +++ b/src/test/abilities/speed_boost.test.ts @@ -0,0 +1,125 @@ +import { Stat } from "#enums/stat"; +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"; +import { CommandPhase } from "#app/phases/command-phase"; +import { Command } from "#app/ui/command-ui-handler"; +import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; + +describe("Abilities - Speed Boost", () => { + 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 + .battleType("single") + .enemySpecies(Species.DRAGAPULT) + .ability(Abilities.SPEED_BOOST) + .enemyMoveset(Moves.SPLASH) + .moveset([ Moves.SPLASH, Moves.U_TURN ]); + }); + + it("should increase speed by 1 stage at end of turn", + async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger this turn if pokemon was switched into combat via attack, but the turn after", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("checking back to back swtiches", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + let playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger this turn if pokemon was switched into combat via normal switch, but the turn after", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger if pokemon fails to escape", + async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const commandPhase = game.scene.getCurrentPhase() as CommandPhase; + commandPhase.handleCommand(Command.RUN, 0); + const runPhase = game.scene.getCurrentPhase() as AttemptRunPhase; + runPhase.forceFailEscape = true; + await game.phaseInterceptor.to(AttemptRunPhase); + await game.toNextTurn(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); +}); diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index cdd2834f588..d34b5631271 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -1,8 +1,8 @@ import { StatusEffect } from "#app/data/status-effect"; -import GameManager from "#app/test/utils/gameManager"; 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"; @@ -30,7 +30,7 @@ describe("Abilities - Synchronize", () => { .enemyAbility(Abilities.SYNCHRONIZE) .moveset([ Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT ]) .ability(Abilities.NO_GUARD); - }, 20000); + }); it("does not trigger when no status is applied by opponent Pokemon", async () => { await game.classicMode.startBattle([ Species.FEEBAS ]); @@ -38,9 +38,9 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.SPLASH); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status).toBeUndefined(); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); - }, 20000); + }); it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => { await game.classicMode.startBattle([ Species.FEEBAS ]); @@ -48,10 +48,10 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.THUNDER_WAVE); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); + }); it("does not trigger on Sleep", async () => { await game.classicMode.startBattle(); @@ -60,10 +60,10 @@ describe("Abilities - Synchronize", () => { await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBeUndefined(); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); - }, 20000); + }); it("does not trigger when Pokemon is statused by Toxic Spikes", async () => { game.override @@ -79,10 +79,10 @@ describe("Abilities - Synchronize", () => { game.doSwitchPokemon(1); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); - expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined(); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.POISON); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBeUndefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); - }, 20000); + }); it("shows ability even if it fails to set the status of the opponent Pokemon", async () => { await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -90,10 +90,10 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.THUNDER_WAVE); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBeUndefined(); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); + }); it("should activate with Psycho Shift after the move clears the status", async () => { game.override.statusEffect(StatusEffect.PARALYSIS); @@ -102,8 +102,8 @@ describe("Abilities - Synchronize", () => { game.move.select(Moves.PSYCHO_SHIFT); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); + }); }); diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts new file mode 100644 index 00000000000..33604efc534 --- /dev/null +++ b/src/test/abilities/unburden.test.ts @@ -0,0 +1,245 @@ +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, vi } from "vitest"; +import { Stat } from "#enums/stat"; +import { BerryType } from "#app/enums/berry-type"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; + + +describe("Abilities - Unburden", () => { + 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 + .battleType("single") + .starterSpecies(Species.TREECKO) + .startingLevel(1) + .moveset([ Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF ]) + .startingHeldItems([ + { name: "BERRY", count: 1, type: BerryType.SITRUS }, + { name: "BERRY", count: 2, type: BerryType.APICOT }, + { name: "BERRY", count: 2, type: BerryType.LUM }, + ]) + .enemySpecies(Species.NINJASK) + .enemyLevel(100) + .enemyMoveset([ Moves.FALSE_SWIPE ]) + .enemyAbility(Abilities.UNBURDEN) + .enemyPassiveAbility(Abilities.NO_GUARD) + .enemyHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + }); + + it("should activate when a berry is eaten", async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.abilityIndex = 2; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + }); + + it("should activate when a berry is stolen", async () => { + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.PLUCK); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is knocked off", async () => { + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.KNOCK_OFF); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via attacking ability", async () => { + game.override + .ability(Abilities.MAGICIAN) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via defending ability", async () => { + game.override + .startingLevel(45) + .enemyAbility(Abilities.PICKPOCKET) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + ]); + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.abilityIndex = 2; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + }); + + it("should activate when an item is stolen via move", async () => { + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); // give Thief 100% steal rate + game.override.startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.THIEF); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via grip claw", async () => { + game.override + .startingLevel(5) + .startingHeldItems([ + { name: "GRIP_CLAW", count: 5 }, + { name: "MULTI_LENS", count: 3 }, + ]) + .enemyHeldItems([ + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + { name: "LEFTOVERS", count: 1 }, + { name: "GRIP_CLAW", count: 1 }, + { name: "MULTI_LENS", count: 1 }, + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + while (enemyPokemon.getHeldItems().length === enemyHeldItemCt) { + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + } + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should not activate when a neutralizing ability is present", async () => { + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + }); + + it("should activate when a move that consumes a berry is used", async () => { + game.override.enemyMoveset([ Moves.STUFF_CHEEKS ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.STUFF_CHEEKS); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should deactivate when a neutralizing gas user enters the field", async () => { + game.override + .battleType("double") + .moveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); + + const playerPokemon = game.scene.getParty(); + const treecko = playerPokemon[0]; + const weezing = playerPokemon[2]; + treecko.abilityIndex = 2; + weezing.abilityIndex = 1; + const playerHeldItems = treecko.getHeldItems().length; + const initialPlayerSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); + await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + }); + +}); diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts new file mode 100644 index 00000000000..6f56a2f4e7e --- /dev/null +++ b/src/test/abilities/wimp_out.test.ts @@ -0,0 +1,614 @@ +import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Wimp Out", () => { + 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 + .battleType("single") + .ability(Abilities.WIMP_OUT) + .enemySpecies(Species.NINJASK) + .enemyPassiveAbility(Abilities.NO_GUARD) + .startingLevel(90) + .enemyLevel(70) + .moveset([ Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE ]) + .enemyMoveset(Moves.FALSE_SWIPE) + .disableCrits(); + }); + + function confirmSwitch(): void { + const [ pokemon1, pokemon2 ] = game.scene.getPlayerParty(); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + + expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD); + + expect(pokemon2.species.speciesId).toBe(Species.WIMPOD); + expect(pokemon2.isFainted()).toBe(false); + expect(pokemon2.getHpRatio()).toBeLessThan(0.5); + } + + function confirmNoSwitch(): void { + const [ pokemon1, pokemon2 ] = game.scene.getPlayerParty(); + + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + + expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD); + + expect(pokemon1.species.speciesId).toBe(Species.WIMPOD); + expect(pokemon1.isFainted()).toBe(false); + expect(pokemon1.getHpRatio()).toBeLessThan(0.5); + } + + it("triggers regenerator passive single time when switching out with wimp out", async () => { + game.override + .passiveAbility(Abilities.REGENERATOR) + .startingLevel(5) + .enemyLevel(100); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod.hp).toEqual(Math.floor(wimpod.getMaxHp() * 0.33 + 1)); + confirmSwitch(); + }); + + it("It makes wild pokemon flee if triggered", async () => { + game.override.enemyAbility(Abilities.WIMP_OUT); + await game.classicMode.startBattle([ + Species.GOLISOPOD, + Species.TYRUNT + ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.hp *= 0.52; + + game.move.select(Moves.FALSE_SWIPE); + await game.phaseInterceptor.to("BerryPhase"); + + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); + }); + + it("Does not trigger when HP already below half", async () => { + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp = 5; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod.hp).toEqual(1); + confirmNoSwitch(); + }); + + it("Trapping moves do not prevent Wimp Out from activating.", async () => { + game.override + .enemyMoveset([ Moves.SPIRIT_SHACKLE ]) + .startingLevel(53) + .enemyLevel(45); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + expect(game.scene.getPlayerParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + confirmSwitch(); + }); + + it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => { + game.override + .startingLevel(95) + .enemyMoveset([ Moves.U_TURN ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const hasFled = enemyPokemon.switchOutStatus; + expect(hasFled).toBe(false); + confirmSwitch(); + }); + + it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => { + game.override + .startingLevel(190) + .startingWave(8) + .enemyMoveset([ Moves.U_TURN ]); + await game.classicMode.startBattle([ + Species.GOLISOPOD, + Species.TYRUNT + ]); + const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1); + }); + + it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => { + game.override + .startingLevel(69) + .enemyMoveset([ Moves.DRAGON_TAIL ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("SwitchSummonPhase", false); + + expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD); + }); + + it("triggers when recoil damage is taken", async () => { + game.override + .moveset([ Moves.HEAD_SMASH ]) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.HEAD_SMASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("It does not activate when the Pokémon cuts its own HP", async () => { + game.override + .moveset([ Moves.SUBSTITUTE ]) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp *= 0.52; + + game.move.select(Moves.SUBSTITUTE); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("Does not trigger when neutralized", async () => { + game.override + .enemyAbility(Abilities.NEUTRALIZING_GAS) + .startingLevel(5); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery", async () => { + game.override + .moveset([ Moves.DOUBLE_EDGE ]) + .enemyMoveset([ Moves.SPLASH ]) + .startingHeldItems([ + { name: "SHELL_BELL", count: 3 }, + { name: "HEALING_CHARM", count: 5 }, + ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.75; + + game.move.select(Moves.DOUBLE_EDGE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerParty()[1].getHpRatio()).toBeGreaterThan(0.5); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT); + }); + + it("Wimp Out will activate due to weather damage", async () => { + game.override + .weather(WeatherType.HAIL) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Does not trigger when enemy has sheer force", async () => { + game.override + .enemyAbility(Abilities.SHEER_FORCE) + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(95); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("Wimp Out will activate due to post turn status damage", async () => { + game.override + .statusEffect(StatusEffect.POISON) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to bad dreams", async () => { + game.override + .statusEffect(StatusEffect.SLEEP) + .enemyAbility(Abilities.BAD_DREAMS); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to leech seed", async () => { + game.override + .enemyMoveset([ Moves.LEECH_SEED ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to curse damage", async () => { + game.override + .enemySpecies(Species.DUSKNOIR) + .enemyMoveset([ Moves.CURSE ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to salt cure damage", async () => { + game.override + .enemySpecies(Species.NACLI) + .enemyMoveset([ Moves.SALT_CURE ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.70; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to damaging trap damage", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.WHIRLPOOL ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.55; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override + .passiveAbility(Abilities.MAGIC_GUARD) + .enemyMoveset([ Moves.LEECH_SEED ]) + .weather(WeatherType.HAIL) + .statusEffect(StatusEffect.POISON); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerParty()[0].getHpRatio()).toEqual(0.51); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); + }); + + it("Wimp Out activating should not cancel a double battle", async () => { + game.override + .battleType("double") + .enemyAbility(Abilities.WIMP_OUT) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const enemyLeadPokemon = game.scene.getEnemyParty()[0]; + const enemySecPokemon = game.scene.getEnemyParty()[1]; + + game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("BerryPhase"); + + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + const hasFledSec = enemySecPokemon.switchOutStatus; + expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); + expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); + expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp()); + }); + + it("Wimp Out will activate due to aftermath", async () => { + game.override + .moveset([ Moves.THUNDER_PUNCH ]) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.AFTERMATH) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.THUNDER_PUNCH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Activates due to entry hazards", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override + .enemySpecies(Species.CENTISKORCH) + .enemyAbility(Abilities.WIMP_OUT) + .startingWave(4); + await game.classicMode.startBattle([ + Species.TYRUNT + ]); + + expect(game.phaseInterceptor.log).not.toContain("MovePhase"); + expect(game.phaseInterceptor.log).toContain("BattleEndPhase"); + }); + + it("Wimp Out will activate due to Nightmare", async () => { + game.override + .enemyMoveset([ Moves.NIGHTMARE ]) + .statusEffect(StatusEffect.SLEEP); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.65; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("triggers status on the wimp out user before a new pokemon is switched in", async () => { + game.override + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(80); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON); + confirmSwitch(); + }); + + it("triggers after last hit of multi hit move", async () => { + game.override + .enemyMoveset(Moves.BULLET_SEED) + .enemyAbility(Abilities.SKILL_LINK); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(5); + confirmSwitch(); + }); + + it("triggers after last hit of multi hit move (multi lens)", async () => { + game.override + .enemyMoveset(Moves.TACKLE) + .enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(2); + confirmSwitch(); + }); + it("triggers after last hit of Parental Bond", async () => { + game.override + .enemyMoveset(Moves.TACKLE) + .enemyAbility(Abilities.PARENTAL_BOND); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(2); + confirmSwitch(); + }); + + // TODO: This interaction is not implemented yet + it.todo("Wimp Out will not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => { + game.override + .moveset([ Moves.SWORDS_DANCE ]) + .enemyMoveset([ Moves.SWAGGER ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.hp *= 0.51; + playerPokemon.setStatStage(Stat.ATK, 6); + playerPokemon.addTag(BattlerTagType.CONFUSED); + + // TODO: add helper function to force confusion self-hits + + while (playerPokemon.getHpRatio() > 0.49) { + game.move.select(Moves.SWORDS_DANCE); + await game.phaseInterceptor.to("TurnEndPhase"); + } + + confirmNoSwitch(); + }); +}); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index 2601954a9a4..74575415788 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -1,29 +1,23 @@ -import { Stat } from "#enums/stat"; import { BattlerIndex } from "#app/battle"; +import { Status, StatusEffect } from "#app/data/status-effect"; import { DamagePhase } from "#app/phases/damage-phase"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { MessagePhase } from "#app/phases/message-phase"; -import { PostSummonPhase } from "#app/phases/post-summon-phase"; -import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; -import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { SwitchType } from "#enums/switch-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { Status, StatusEffect } from "#app/data/status-effect"; -import { SwitchType } from "#enums/switch-type"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Abilities - ZEN MODE", () => { let phaserGame: Phaser.Game; let game: GameManager; + const baseForm = 0; + const zenForm = 1; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -37,121 +31,104 @@ describe("Abilities - ZEN MODE", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = Moves.SPLASH; - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.HYDRATION); - game.override.ability(Abilities.ZEN_MODE); - game.override.startingLevel(100); - game.override.moveset([ moveToUse ]); - game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); + game.override + .battleType("single") + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.HYDRATION) + .ability(Abilities.ZEN_MODE) + .startingLevel(100) + .moveset(Moves.SPLASH) + .enemyMoveset(Moves.TACKLE); }); - test( - "not enough damage to change form", - async () => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ Species.DARMANITAN ]); - game.scene.getParty()[0].stats[Stat.HP] = 100; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + it("shouldn't change form when taking damage if not dropping below 50% HP", async () => { + await game.classicMode.startBattle([ Species.DARMANITAN ]); + const player = game.scene.getPlayerPokemon()!; + player.stats[Stat.HP] = 100; + player.hp = 100; + expect(player.formIndex).toBe(baseForm); - game.move.select(moveToUse); + game.move.select(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(DamagePhase, false); - // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(40); - await game.phaseInterceptor.runFrom(DamagePhase).to(TurnEndPhase, false); - expect(game.scene.getParty()[0].hp).toBeLessThan(100); - expect(game.scene.getParty()[0].formIndex).toBe(0); - }, - ); + expect(player.hp).toBeLessThan(100); + expect(player.formIndex).toBe(baseForm); + }); - test( - "enough damage to change form", - async () => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ Species.DARMANITAN ]); - game.scene.getParty()[0].stats[Stat.HP] = 1000; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + it("should change form when falling below 50% HP", async () => { + await game.classicMode.startBattle([ Species.DARMANITAN ]); - game.move.select(moveToUse); + const player = game.scene.getPlayerPokemon()!; + player.stats[Stat.HP] = 1000; + player.hp = 100; + expect(player.formIndex).toBe(baseForm); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(QuietFormChangePhase); - await game.phaseInterceptor.to(TurnInitPhase, false); - expect(game.scene.getParty()[0].hp).not.toBe(100); - expect(game.scene.getParty()[0].formIndex).not.toBe(0); - }, - ); + game.move.select(Moves.SPLASH); - test( - "kill pokemon while on zen mode", - async () => { - const moveToUse = Moves.SPLASH; - await game.startBattle([ Species.DARMANITAN, Species.CHARIZARD ]); - game.scene.getParty()[0].stats[Stat.HP] = 1000; - game.scene.getParty()[0].hp = 100; - expect(game.scene.getParty()[0].formIndex).toBe(0); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("QuietFormChangePhase"); + await game.phaseInterceptor.to("TurnInitPhase", false); - game.move.select(moveToUse); + expect(player.hp).not.toBe(100); + expect(player.formIndex).toBe(zenForm); + }); - await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to(DamagePhase, false); - // await game.phaseInterceptor.runFrom(DamagePhase).to(DamagePhase, false); - const damagePhase = game.scene.getCurrentPhase() as DamagePhase; - damagePhase.updateAmount(80); - await game.phaseInterceptor.runFrom(DamagePhase).to(QuietFormChangePhase); - expect(game.scene.getParty()[0].hp).not.toBe(100); - expect(game.scene.getParty()[0].formIndex).not.toBe(0); - await game.killPokemon(game.scene.getParty()[0]); - expect(game.scene.getParty()[0].isFainted()).toBe(true); - await game.phaseInterceptor.run(MessagePhase); - await game.phaseInterceptor.run(EnemyCommandPhase); - await game.phaseInterceptor.run(TurnStartPhase); - game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { - game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); - game.scene.ui.setMode(Mode.MESSAGE); - }); - game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { - game.endPhase(); - }); - await game.phaseInterceptor.run(SwitchPhase); - await game.phaseInterceptor.to(PostSummonPhase); - expect(game.scene.getParty()[1].formIndex).toBe(1); - }, - ); + it("should stay zen mode when fainted", async () => { + await game.classicMode.startBattle([ Species.DARMANITAN, Species.CHARIZARD ]); + const player = game.scene.getPlayerPokemon()!; + player.stats[Stat.HP] = 1000; + player.hp = 100; + expect(player.formIndex).toBe(baseForm); - test( - "check if fainted pokemon switches to base form on arena reset", - async () => { - const baseForm = 0, - zenForm = 1; - game.override.startingWave(4); - game.override.starterForms({ - [Species.DARMANITAN]: zenForm, - }); + game.move.select(Moves.SPLASH); - await game.startBattle([ Species.MAGIKARP, Species.DARMANITAN ]); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to(DamagePhase, false); + const damagePhase = game.scene.getCurrentPhase() as DamagePhase; + damagePhase.updateAmount(80); + await game.phaseInterceptor.to("QuietFormChangePhase"); - const darmanitan = game.scene.getParty().find((p) => p.species.speciesId === Species.DARMANITAN)!; - expect(darmanitan).not.toBe(undefined); - expect(darmanitan.formIndex).toBe(zenForm); + expect(player.hp).not.toBe(100); + expect(player.formIndex).toBe(zenForm); - darmanitan.hp = 0; - darmanitan.status = new Status(StatusEffect.FAINT); - expect(darmanitan.isFainted()).toBe(true); + await game.killPokemon(player); + expect(player.isFainted()).toBe(true); - game.move.select(Moves.SPLASH); - await game.doKillOpponents(); - await game.phaseInterceptor.to(TurnEndPhase); - game.doSelectModifier(); - await game.phaseInterceptor.to(QuietFormChangePhase); + await game.phaseInterceptor.to("TurnStartPhase"); + game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { + game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); + game.scene.ui.setMode(Mode.MESSAGE); + }); + game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + await game.phaseInterceptor.to("PostSummonPhase"); - expect(darmanitan.formIndex).toBe(baseForm); - }, - ); + expect(game.scene.getPlayerParty()[1].formIndex).toBe(zenForm); + }); + + it("should switch to base form on arena reset", async () => { + game.override.startingWave(4); + game.override.starterForms({ + [Species.DARMANITAN]: zenForm, + }); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.DARMANITAN ]); + + const darmanitan = game.scene.getPlayerParty().find((p) => p.species.speciesId === Species.DARMANITAN)!; + expect(darmanitan.formIndex).toBe(zenForm); + + darmanitan.hp = 0; + darmanitan.status = new Status(StatusEffect.FAINT); + expect(darmanitan.isFainted()).toBe(true); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("TurnEndPhase"); + game.doSelectModifier(); + await game.phaseInterceptor.to("QuietFormChangePhase"); + + expect(darmanitan.formIndex).toBe(baseForm); + }); }); diff --git a/src/test/abilities/zero_to_hero.test.ts b/src/test/abilities/zero_to_hero.test.ts index 48a451e99a2..160b43abf1e 100644 --- a/src/test/abilities/zero_to_hero.test.ts +++ b/src/test/abilities/zero_to_hero.test.ts @@ -41,8 +41,8 @@ describe("Abilities - ZERO TO HERO", () => { await game.startBattle([ Species.FEEBAS, Species.PALAFIN, Species.PALAFIN ]); - const palafin1 = game.scene.getParty()[1]; - const palafin2 = game.scene.getParty()[2]; + const palafin1 = game.scene.getPlayerParty()[1]; + const palafin2 = game.scene.getPlayerParty()[2]; expect(palafin1.formIndex).toBe(heroForm); expect(palafin2.formIndex).toBe(heroForm); palafin2.hp = 0; diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index eed76397f85..656cc62ac59 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -136,9 +136,9 @@ describe("Test Battle Phase", () => { Species.CHANSEY, Species.MEW ]); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CHARIZARD); - expect(game.scene.getParty()[1].species.speciesId).toBe(Species.CHANSEY); - expect(game.scene.getParty()[2].species.speciesId).toBe(Species.MEW); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.CHARIZARD); + expect(game.scene.getPlayerParty()[1].species.speciesId).toBe(Species.CHANSEY); + expect(game.scene.getPlayerParty()[2].species.speciesId).toBe(Species.MEW); }, 20000); it("test remove random battle seed int", async () => { diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index b7a5616d642..a7585d55bab 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -49,7 +49,7 @@ describe("Double Battles", () => { await game.phaseInterceptor.to(BattleEndPhase); game.doSelectModifier(); - const charizard = game.scene.getParty().findIndex(p => p.species.speciesId === Species.CHARIZARD); + const charizard = game.scene.getPlayerParty().findIndex(p => p.species.speciesId === Species.CHARIZARD); game.doRevivePokemon(charizard); await game.phaseInterceptor.to(TurnInitPhase); diff --git a/src/test/daily_mode.test.ts b/src/test/daily_mode.test.ts index 100cf07f9c0..2a88ce10ae7 100644 --- a/src/test/daily_mode.test.ts +++ b/src/test/daily_mode.test.ts @@ -30,7 +30,7 @@ describe("Daily Mode", () => { it("should initialize properly", async () => { await game.dailyMode.runToSummon(); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); expect(party).toHaveLength(3); party.forEach(pkm => { expect(pkm.level).toBe(20); diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index b94d45d6537..3046d103cbc 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -35,8 +35,8 @@ describe("Evolution", () => { it("should keep hidden ability after evolving", async () => { await game.classicMode.runToSummon([ Species.EEVEE, Species.TRAPINCH ]); - const eevee = game.scene.getParty()[0]; - const trapinch = game.scene.getParty()[1]; + const eevee = game.scene.getPlayerParty()[0]; + const trapinch = game.scene.getPlayerParty()[1]; eevee.abilityIndex = 2; trapinch.abilityIndex = 2; @@ -50,8 +50,8 @@ describe("Evolution", () => { it("should keep same ability slot after evolving", async () => { await game.classicMode.runToSummon([ Species.BULBASAUR, Species.CHARMANDER ]); - const bulbasaur = game.scene.getParty()[0]; - const charmander = game.scene.getParty()[1]; + const bulbasaur = game.scene.getPlayerParty()[0]; + const charmander = game.scene.getPlayerParty()[1]; bulbasaur.abilityIndex = 0; charmander.abilityIndex = 1; @@ -80,8 +80,8 @@ describe("Evolution", () => { nincada.metBiome = -1; nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); - const ninjask = game.scene.getParty()[0]; - const shedinja = game.scene.getParty()[1]; + const ninjask = game.scene.getPlayerParty()[0]; + const shedinja = game.scene.getPlayerParty()[1]; expect(ninjask.abilityIndex).toBe(2); expect(shedinja.abilityIndex).toBe(1); // Regression test for https://github.com/pagefaultgames/pokerogue/issues/3842 diff --git a/src/test/field/pokemon.test.ts b/src/test/field/pokemon.test.ts index aeaecab5874..ce9862e22e4 100644 --- a/src/test/field/pokemon.test.ts +++ b/src/test/field/pokemon.test.ts @@ -45,7 +45,7 @@ describe("Spec - Pokemon", () => { const zubat = scene.getEnemyPokemon()!; zubat.addToParty(PokeballType.LUXURY_BALL); - const party = scene.getParty(); + const party = scene.getPlayerParty(); expect(party).toHaveLength(6); party.forEach((pkm, index) =>{ expect(pkm.species.speciesId).toBe(index === 5 ? Species.ZUBAT : Species.ABRA); @@ -57,7 +57,7 @@ describe("Spec - Pokemon", () => { const zubat = scene.getEnemyPokemon()!; zubat.addToParty(PokeballType.LUXURY_BALL, slotIndex); - const party = scene.getParty(); + const party = scene.getPlayerParty(); expect(party).toHaveLength(6); party.forEach((pkm, index) =>{ expect(pkm.species.speciesId).toBe(index === slotIndex ? Species.ZUBAT : Species.ABRA); diff --git a/src/test/items/eviolite.test.ts b/src/test/items/eviolite.test.ts index 7b2f9a15fce..a97c287da29 100644 --- a/src/test/items/eviolite.test.ts +++ b/src/test/items/eviolite.test.ts @@ -1,10 +1,10 @@ -import { Stat } from "#enums/stat"; +import { StatBoosterModifier } from "#app/modifier/modifier"; +import { NumberHolder, randItem } from "#app/utils"; import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import GameManager from "#test/utils/gameManager"; import Phase from "phaser"; -import * as Utils from "#app/utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { StatBoosterModifier } from "#app/modifier/modifier"; describe("Items - Eviolite", () => { let phaserGame: Phaser.Game; @@ -28,14 +28,12 @@ describe("Items - Eviolite", () => { }); it("should provide 50% boost to DEF and SPDEF for unevolved, unfused pokemon", async() => { - await game.classicMode.startBattle([ - Species.PICHU - ]); + await game.classicMode.startBattle([ Species.PICHU ]); const partyMember = game.scene.getPlayerPokemon()!; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -51,14 +49,12 @@ describe("Items - Eviolite", () => { }); it("should not provide a boost for fully evolved, unfused pokemon", async() => { - await game.classicMode.startBattle([ - Species.RAICHU, - ]); + await game.classicMode.startBattle([ Species.RAICHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerPokemon()!; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -75,12 +71,9 @@ describe("Items - Eviolite", () => { }); it("should provide 50% boost to DEF and SPDEF for completely unevolved, fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.PICHU, - Species.CLEFFA - ]); + await game.classicMode.startBattle([ Species.PICHU, Species.CLEFFA ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -92,7 +85,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -108,12 +101,9 @@ describe("Items - Eviolite", () => { }); it("should provide 25% boost to DEF and SPDEF for partially unevolved (base), fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.PICHU, - Species.CLEFABLE - ]); + await game.classicMode.startBattle([ Species.PICHU, Species.CLEFABLE ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -125,7 +115,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -141,12 +131,9 @@ describe("Items - Eviolite", () => { }); it("should provide 25% boost to DEF and SPDEF for partially unevolved (fusion), fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.RAICHU, - Species.CLEFFA - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.CLEFFA ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -158,7 +145,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -174,12 +161,9 @@ describe("Items - Eviolite", () => { }); it("should not provide a boost for fully evolved, fused pokemon", async() => { - await game.classicMode.startBattle([ - Species.RAICHU, - Species.CLEFABLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.CLEFABLE ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -191,7 +175,7 @@ describe("Items - Eviolite", () => { partyMember.fusionLuck = ally.luck; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity @@ -216,14 +200,12 @@ describe("Items - Eviolite", () => { const gMaxablePokemon = [ Species.PIKACHU, Species.EEVEE, Species.DURALUDON, Species.MEOWTH ]; - await game.classicMode.startBattle([ - Utils.randItem(gMaxablePokemon) - ]); + await game.classicMode.startBattle([ randItem(gMaxablePokemon) ]); const partyMember = game.scene.getPlayerPokemon()!; vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => { - const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false)); + const statValue = new NumberHolder(partyMember.getStat(stat, false)); game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue); // Ignore other calculations for simplicity diff --git a/src/test/items/leek.test.ts b/src/test/items/leek.test.ts index e27462a9265..901b353b3d3 100644 --- a/src/test/items/leek.test.ts +++ b/src/test/items/leek.test.ts @@ -89,7 +89,7 @@ describe("Items - Leek", () => { Species.PIKACHU, ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -120,7 +120,7 @@ describe("Items - Leek", () => { species[Utils.randInt(species.length)] ]); - const [ partyMember, ally ] = game.scene.getParty(); + const [ partyMember, ally ] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; diff --git a/src/test/items/light_ball.test.ts b/src/test/items/light_ball.test.ts index 78375487f3b..fe79b6a2045 100644 --- a/src/test/items/light_ball.test.ts +++ b/src/test/items/light_ball.test.ts @@ -35,7 +35,7 @@ describe("Items - Light Ball", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Light Ball is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Light Ball", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); const spAtkStat = partyMember.getStat(Stat.SPATK); @@ -97,8 +97,8 @@ describe("Items - Light Ball", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -136,8 +136,8 @@ describe("Items - Light Ball", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -174,7 +174,7 @@ describe("Items - Light Ball", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); const spAtkStat = partyMember.getStat(Stat.SPATK); diff --git a/src/test/items/metal_powder.test.ts b/src/test/items/metal_powder.test.ts index c577182f350..86e7d329ecb 100644 --- a/src/test/items/metal_powder.test.ts +++ b/src/test/items/metal_powder.test.ts @@ -35,7 +35,7 @@ describe("Items - Metal Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Metal Powder is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Metal Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const defStat = partyMember.getStat(Stat.DEF); @@ -91,8 +91,8 @@ describe("Items - Metal Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -124,8 +124,8 @@ describe("Items - Metal Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -156,7 +156,7 @@ describe("Items - Metal Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const defStat = partyMember.getStat(Stat.DEF); diff --git a/src/test/items/quick_powder.test.ts b/src/test/items/quick_powder.test.ts index 4eb6f6fb164..905d023ad8b 100644 --- a/src/test/items/quick_powder.test.ts +++ b/src/test/items/quick_powder.test.ts @@ -35,7 +35,7 @@ describe("Items - Quick Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Quick Powder is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Quick Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const spdStat = partyMember.getStat(Stat.SPD); @@ -91,8 +91,8 @@ describe("Items - Quick Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -124,8 +124,8 @@ describe("Items - Quick Powder", () => { Species.DITTO ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -156,7 +156,7 @@ describe("Items - Quick Powder", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const spdStat = partyMember.getStat(Stat.SPD); diff --git a/src/test/items/thick_club.test.ts b/src/test/items/thick_club.test.ts index 74158089c77..d349a1ad7b9 100644 --- a/src/test/items/thick_club.test.ts +++ b/src/test/items/thick_club.test.ts @@ -35,7 +35,7 @@ describe("Items - Thick Club", () => { Species.CUBONE ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; // Checking console log to make sure Thick Club is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -68,7 +68,7 @@ describe("Items - Thick Club", () => { Species.CUBONE ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); @@ -90,7 +90,7 @@ describe("Items - Thick Club", () => { Species.MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); @@ -112,7 +112,7 @@ describe("Items - Thick Club", () => { Species.ALOLA_MAROWAK ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); @@ -139,8 +139,8 @@ describe("Items - Thick Club", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -176,8 +176,8 @@ describe("Items - Thick Club", () => { species[randSpecies] ]); - const partyMember = game.scene.getParty()[0]; - const ally = game.scene.getParty()[1]; + const partyMember = game.scene.getPlayerParty()[0]; + const ally = game.scene.getPlayerParty()[1]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -208,7 +208,7 @@ describe("Items - Thick Club", () => { Species.PIKACHU ]); - const partyMember = game.scene.getParty()[0]; + const partyMember = game.scene.getPlayerParty()[0]; const atkStat = partyMember.getStat(Stat.ATK); diff --git a/src/test/moves/aromatherapy.test.ts b/src/test/moves/aromatherapy.test.ts index f547ed0e54c..874dadc0a1f 100644 --- a/src/test/moves/aromatherapy.test.ts +++ b/src/test/moves/aromatherapy.test.ts @@ -33,7 +33,7 @@ describe("Moves - Aromatherapy", () => { it("should cure status effect of the user, its ally, and all party pokemon", async () => { await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); @@ -79,7 +79,7 @@ describe("Moves - Aromatherapy", () => { it("should not cure status effect of allies ON FIELD with Sap Sipper, should still cure allies in party", async () => { game.override.ability(Abilities.SAP_SIPPER); await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); diff --git a/src/test/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts index 9d4a9358715..52e4c3ec016 100644 --- a/src/test/moves/baton_pass.test.ts +++ b/src/test/moves/baton_pass.test.ts @@ -95,7 +95,7 @@ describe("Moves - Baton Pass", () => { game.override.enemyMoveset([ Moves.SALT_CURE ]); await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]); - const [ player1, player2 ] = game.scene.getParty(); + const [ player1, player2 ] = game.scene.getPlayerParty(); game.move.select(Moves.BATON_PASS); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); diff --git a/src/test/moves/beat_up.test.ts b/src/test/moves/beat_up.test.ts index e51ac6ea452..a0129621f0e 100644 --- a/src/test/moves/beat_up.test.ts +++ b/src/test/moves/beat_up.test.ts @@ -65,7 +65,7 @@ describe("Moves - Beat Up", () => { const playerPokemon = game.scene.getPlayerPokemon()!; - game.scene.getParty()[1].trySetStatus(StatusEffect.BURN); + game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN); game.move.select(Moves.BEAT_UP); diff --git a/src/test/moves/camouflage.test.ts b/src/test/moves/camouflage.test.ts new file mode 100644 index 00000000000..acf37635c47 --- /dev/null +++ b/src/test/moves/camouflage.test.ts @@ -0,0 +1,49 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TerrainType } from "#app/data/terrain"; +import { Type } from "#app/data/type"; +import { BattlerIndex } from "#app/battle"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Camouflage", () => { + 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.CAMOUFLAGE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.REGIELEKI) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.PSYCHIC_TERRAIN); + }); + + it("Camouflage should look at terrain first when selecting a type to change into", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.CAMOUFLAGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTerrainType()).toBe(TerrainType.PSYCHIC); + const pokemonType = playerPokemon.getTypes()[0]; + expect(pokemonType).toBe(Type.PSYCHIC); + }); +}); diff --git a/src/test/moves/chloroblast.test.ts b/src/test/moves/chloroblast.test.ts new file mode 100644 index 00000000000..5e55bf46958 --- /dev/null +++ b/src/test/moves/chloroblast.test.ts @@ -0,0 +1,42 @@ +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 - Chloroblast", () => { + 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.CHLOROBLAST ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.PROTECT); + }); + + it("should not deal recoil damage if the opponent uses protect", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.CHLOROBLAST); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.isFullHp()).toBe(true); + }); +}); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index dcbed7107e6..595f347a6b5 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -48,7 +48,7 @@ describe("Moves - Dragon Rage", () => { await game.startBattle(); - partyPokemon = game.scene.getParty()[0]; + partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; // remove berries diff --git a/src/test/moves/dragon_tail.test.ts b/src/test/moves/dragon_tail.test.ts index cf801eb42c1..6b3e669f770 100644 --- a/src/test/moves/dragon_tail.test.ts +++ b/src/test/moves/dragon_tail.test.ts @@ -73,7 +73,7 @@ describe("Moves - Dragon Tail", () => { .enemyAbility(Abilities.ROUGH_SKIN); await game.classicMode.startBattle([ Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD ]); - const leadPokemon = game.scene.getParty()[0]!; + const leadPokemon = game.scene.getPlayerParty()[0]!; const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; const enemySecPokemon = game.scene.getEnemyParty()[1]!; @@ -105,8 +105,8 @@ describe("Moves - Dragon Tail", () => { .enemyAbility(Abilities.ROUGH_SKIN); await game.classicMode.startBattle([ Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD ]); - const leadPokemon = game.scene.getParty()[0]!; - const secPokemon = game.scene.getParty()[1]!; + const leadPokemon = game.scene.getPlayerParty()[0]!; + const secPokemon = game.scene.getPlayerParty()[1]!; const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; const enemySecPokemon = game.scene.getEnemyParty()[1]!; diff --git a/src/test/moves/dynamax_cannon.test.ts b/src/test/moves/dynamax_cannon.test.ts index 9dd48d3c94c..001f986bd52 100644 --- a/src/test/moves/dynamax_cannon.test.ts +++ b/src/test/moves/dynamax_cannon.test.ts @@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); }, 20000); @@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); }, 20000); @@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); }, 20000); @@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); }, 20000); @@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); }, 20000); diff --git a/src/test/moves/fairy_lock.test.ts b/src/test/moves/fairy_lock.test.ts new file mode 100644 index 00000000000..ceb298ed0fe --- /dev/null +++ b/src/test/moves/fairy_lock.test.ts @@ -0,0 +1,152 @@ +import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +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 - Fairy Lock", () => { + 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.FAIRY_LOCK, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH, Moves.U_TURN ]); + }); + + it("Applies Fairy Lock tag for two turns", async () => { + await game.classicMode.startBattle([ Species.KLEFKI, Species.TYRUNT ]); + const playerPokemon = game.scene.getPlayerField(); + const enemyField = game.scene.getEnemyField(); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(playerPokemon[0].isTrapped()).toEqual(true); + expect(playerPokemon[1].isTrapped()).toEqual(true); + expect(enemyField[0].isTrapped()).toEqual(true); + expect(enemyField[1].isTrapped()).toEqual(true); + + await game.toNextTurn(); + expect(playerPokemon[0].isTrapped()).toEqual(false); + expect(playerPokemon[1].isTrapped()).toEqual(false); + expect(enemyField[0].isTrapped()).toEqual(false); + expect(enemyField[1].isTrapped()).toEqual(false); + }); + + it("Ghost types can escape Fairy Lock", async () => { + await game.classicMode.startBattle([ Species.DUSKNOIR, Species.GENGAR, Species.TYRUNT ]); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[0].isTrapped()).toEqual(false); + expect(game.scene.getPlayerField()[1].isTrapped()).toEqual(false); + + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[1].species.speciesId).not.toBe(Species.GENGAR); + }); + + it("Phasing moves will still switch out", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ]); + await game.classicMode.startBattle([ Species.KLEFKI, Species.TYRUNT, Species.ZYGARDE ]); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.WHIRLWIND, 0); + game.doSelectPartyPokemon(2); + await game.forceEnemyMove(Moves.WHIRLWIND, 1); + game.doSelectPartyPokemon(2); + await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()[0].species.speciesId).not.toBe(Species.KLEFKI); + expect(game.scene.getPlayerField()[1].species.speciesId).not.toBe(Species.TYRUNT); + }); + + it("If a Pokemon faints and is replaced the replacement is also trapped", async () => { + game.override.moveset([ Moves.FAIRY_LOCK, Moves.SPLASH, Moves.MEMENTO ]); + await game.classicMode.startBattle([ Species.KLEFKI, Species.GUZZLORD, Species.TYRUNT, Species.ZYGARDE ]); + + game.move.select(Moves.FAIRY_LOCK); + game.move.select(Moves.MEMENTO, 1); + game.doSelectPartyPokemon(2); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.ENEMY)).toBeDefined(); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerField()[0].isTrapped()).toEqual(true); + expect(game.scene.getPlayerField()[1].isTrapped()).toEqual(true); + expect(game.scene.getEnemyField()[0].isTrapped()).toEqual(true); + expect(game.scene.getEnemyField()[1].isTrapped()).toEqual(true); + + await game.toNextTurn(); + expect(game.scene.getPlayerField()[0].isTrapped()).toEqual(false); + expect(game.scene.getPlayerField()[1].isTrapped()).toEqual(false); + expect(game.scene.getEnemyField()[0].isTrapped()).toEqual(false); + expect(game.scene.getEnemyField()[1].isTrapped()).toEqual(false); + }); +}); diff --git a/src/test/moves/fissure.test.ts b/src/test/moves/fissure.test.ts index 16c3faa6827..12f075f1b55 100644 --- a/src/test/moves/fissure.test.ts +++ b/src/test/moves/fissure.test.ts @@ -43,7 +43,7 @@ describe("Moves - Fissure", () => { await game.startBattle(); - partyPokemon = game.scene.getParty()[0]; + partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; // remove berries diff --git a/src/test/moves/focus_punch.test.ts b/src/test/moves/focus_punch.test.ts index 386eb2537ee..352e3b60aa4 100644 --- a/src/test/moves/focus_punch.test.ts +++ b/src/test/moves/focus_punch.test.ts @@ -59,7 +59,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); - expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp); + expect(leadPokemon.turnData.totalDamageDealt).toBe(enemyStartingHp - enemyPokemon.hp); } ); @@ -86,7 +86,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBe(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); - expect(leadPokemon.turnData.damageDealt).toBe(0); + expect(leadPokemon.turnData.totalDamageDealt).toBe(0); } ); diff --git a/src/test/moves/forests_curse.test.ts b/src/test/moves/forests_curse.test.ts new file mode 100644 index 00000000000..c49bdab5255 --- /dev/null +++ b/src/test/moves/forests_curse.test.ts @@ -0,0 +1,47 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Forest's Curse", () => { + 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.FORESTS_CURSE, Moves.TRICK_OR_TREAT ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("will replace the added type from Trick Or Treat", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + game.move.select(Moves.TRICK_OR_TREAT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon!.summonData.addedType).toBe(Type.GHOST); + + game.move.select(Moves.FORESTS_CURSE); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon?.summonData.addedType).toBe(Type.GRASS); + }); +}); diff --git a/src/test/moves/fusion_flare_bolt.test.ts b/src/test/moves/fusion_flare_bolt.test.ts index 0d9b9898276..1bcd0514357 100644 --- a/src/test/moves/fusion_flare_bolt.test.ts +++ b/src/test/moves/fusion_flare_bolt.test.ts @@ -176,7 +176,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { Species.ZEKROM ]); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const enemyParty = game.scene.getEnemyParty(); // Get rid of any modifiers that may alter power @@ -235,7 +235,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { Species.ZEKROM ]); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const enemyParty = game.scene.getEnemyParty(); // Get rid of any modifiers that may alter power diff --git a/src/test/moves/heal_bell.test.ts b/src/test/moves/heal_bell.test.ts index e4a019d9c0e..b180588d3a3 100644 --- a/src/test/moves/heal_bell.test.ts +++ b/src/test/moves/heal_bell.test.ts @@ -33,7 +33,7 @@ describe("Moves - Heal Bell", () => { it("should cure status effect of the user, its ally, and all party pokemon", async () => { await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); @@ -79,7 +79,7 @@ describe("Moves - Heal Bell", () => { it("should not cure status effect of allies ON FIELD with Soundproof, should still cure allies in party", async () => { game.override.ability(Abilities.SOUNDPROOF); await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); vi.spyOn(leftPlayer, "resetStatus"); vi.spyOn(rightPlayer, "resetStatus"); diff --git a/src/test/moves/magnet_rise.test.ts b/src/test/moves/magnet_rise.test.ts index 0afcad67ea3..b26bbf42ed0 100644 --- a/src/test/moves/magnet_rise.test.ts +++ b/src/test/moves/magnet_rise.test.ts @@ -35,10 +35,10 @@ describe("Moves - Magnet Rise", () => { it("MAGNET RISE", async () => { await game.startBattle(); - const startingHp = game.scene.getParty()[0].hp; + const startingHp = game.scene.getPlayerParty()[0].hp; game.move.select(moveToUse); await game.phaseInterceptor.to(TurnEndPhase); - const finalHp = game.scene.getParty()[0].hp; + const finalHp = game.scene.getPlayerParty()[0].hp; const hpLost = finalHp - startingHp; expect(hpLost).toBe(0); }, 20000); @@ -46,15 +46,15 @@ describe("Moves - Magnet Rise", () => { it("MAGNET RISE - Gravity", async () => { await game.startBattle(); - const startingHp = game.scene.getParty()[0].hp; + const startingHp = game.scene.getPlayerParty()[0].hp; game.move.select(moveToUse); await game.phaseInterceptor.to(CommandPhase); - let finalHp = game.scene.getParty()[0].hp; + let finalHp = game.scene.getPlayerParty()[0].hp; let hpLost = finalHp - startingHp; expect(hpLost).toBe(0); game.move.select(Moves.GRAVITY); await game.phaseInterceptor.to(TurnEndPhase); - finalHp = game.scene.getParty()[0].hp; + finalHp = game.scene.getPlayerParty()[0].hp; hpLost = finalHp - startingHp; expect(hpLost).not.toBe(0); }, 20000); diff --git a/src/test/moves/multi_target.test.ts b/src/test/moves/multi_target.test.ts index b6836b1dcb9..965876d3445 100644 --- a/src/test/moves/multi_target.test.ts +++ b/src/test/moves/multi_target.test.ts @@ -79,7 +79,7 @@ describe("Multi-target damage reduction", () => { it("should reduce earthquake when more than one pokemon other than user is not fainted", async () => { await game.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); - const player2 = game.scene.getParty()[1]; + const player2 = game.scene.getPlayerParty()[1]; const [ enemy1, enemy2 ] = game.scene.getEnemyField(); game.move.select(Moves.EARTHQUAKE); diff --git a/src/test/moves/parting_shot.test.ts b/src/test/moves/parting_shot.test.ts index e5a2fc38b94..cfdf6c15966 100644 --- a/src/test/moves/parting_shot.test.ts +++ b/src/test/moves/parting_shot.test.ts @@ -84,19 +84,19 @@ describe("Moves - Parting Shot", () => { // use Memento 3 times to debuff enemy game.move.select(Moves.MEMENTO); await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); game.doSelectPartyPokemon(1); await game.phaseInterceptor.to(TurnInitPhase, false); game.move.select(Moves.MEMENTO); await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); game.doSelectPartyPokemon(2); await game.phaseInterceptor.to(TurnInitPhase, false); game.move.select(Moves.MEMENTO); await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); game.doSelectPartyPokemon(3); // set up done @@ -182,8 +182,8 @@ describe("Moves - Parting Shot", () => { game.move.select(Moves.SPLASH); // intentionally kill party pokemon, switch to second slot (now 1 party mon is fainted) - await game.killPokemon(game.scene.getParty()[0]); - expect(game.scene.getParty()[0].isFainted()).toBe(true); + await game.killPokemon(game.scene.getPlayerParty()[0]); + expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); await game.phaseInterceptor.run(MessagePhase); game.doSelectPartyPokemon(1); diff --git a/src/test/moves/reflect_type.test.ts b/src/test/moves/reflect_type.test.ts new file mode 100644 index 00000000000..0e47d4b00fc --- /dev/null +++ b/src/test/moves/reflect_type.test.ts @@ -0,0 +1,59 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Reflect Type", () => { + 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 + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemyAbility(Abilities.BALL_FETCH); + }); + + it("will make the user Normal/Grass if targetting a typeless Pokemon affected by Forest's Curse", async () => { + game.override + .moveset([ Moves.FORESTS_CURSE, Moves.REFLECT_TYPE ]) + .startingLevel(60) + .enemySpecies(Species.CHARMANDER) + .enemyMoveset([ Moves.BURN_UP, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.BURN_UP); + await game.toNextTurn(); + + game.move.select(Moves.FORESTS_CURSE); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(enemyPokemon?.getTypes().includes(Type.UNKNOWN)).toBe(true); + expect(enemyPokemon?.getTypes().includes(Type.GRASS)).toBe(true); + + game.move.select(Moves.REFLECT_TYPE); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon?.getTypes()[0]).toBe(Type.NORMAL); + expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true); + }); +}); diff --git a/src/test/moves/rollout.test.ts b/src/test/moves/rollout.test.ts index 4a14b100f6d..199f4e1dcf2 100644 --- a/src/test/moves/rollout.test.ts +++ b/src/test/moves/rollout.test.ts @@ -44,7 +44,7 @@ describe("Moves - Rollout", () => { await game.startBattle(); - const playerPkm = game.scene.getParty()[0]; + const playerPkm = game.scene.getPlayerParty()[0]; vi.spyOn(playerPkm, "stats", "get").mockReturnValue([ 500000, 1, 1, 1, 1, 1 ]); // HP, ATK, DEF, SPATK, SPDEF, SPD const enemyPkm = game.scene.getEnemyParty()[0]; diff --git a/src/test/moves/round.test.ts b/src/test/moves/round.test.ts new file mode 100644 index 00000000000..fd318d30c1e --- /dev/null +++ b/src/test/moves/round.test.ts @@ -0,0 +1,65 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +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, vi } from "vitest"; + +describe("Moves - Round", () => { + 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.ROUND ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.SPLASH, Moves.ROUND ]) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should cue other instances of Round together in Speed order", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const round = allMoves[Moves.ROUND]; + const spy = vi.spyOn(round, "calculateBattlePower"); + + game.move.select(Moves.ROUND, 0, BattlerIndex.ENEMY); + game.move.select(Moves.ROUND, 1, BattlerIndex.ENEMY_2); + + await game.forceEnemyMove(Moves.ROUND, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY ]); + + const actualTurnOrder: BattlerIndex[] = []; + + for (let i = 0; i < 4; i++) { + await game.phaseInterceptor.to("MoveEffectPhase", false); + actualTurnOrder.push((game.scene.getCurrentPhase() as MoveEffectPhase).getUserPokemon()!.getBattlerIndex()); + await game.phaseInterceptor.to("MoveEndPhase"); + } + + expect(actualTurnOrder).toEqual([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + const powerResults = spy.mock.results.map(result => result.value); + expect(powerResults).toEqual( [ 60, 120, 120 ]); + }); +}); diff --git a/src/test/moves/secret_power.test.ts b/src/test/moves/secret_power.test.ts index ff0b5ae8c24..09fe5faa50b 100644 --- a/src/test/moves/secret_power.test.ts +++ b/src/test/moves/secret_power.test.ts @@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Stat } from "#enums/stat"; -import { allMoves, SecretPowerAttr } from "#app/data/move"; +import { allMoves } from "#app/data/move"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#app/battle"; import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagSide } from "#app/data/arena-tag"; +import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; describe("Moves - Secret Power", () => { let phaserGame: Phaser.Game; @@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); }); - it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect", + it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge", async () => { game.override .moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ]) + .ability(Abilities.SERENE_GRACE) .enemyMoveset([ Moves.SPLASH ]) .battleType("double"); await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]); - const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0]; - vi.spyOn(secretPowerAttr, "getMoveChance"); + const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0]; + vi.spyOn(sereneGraceAttr, "apply"); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined(); + let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER); + expect(rainbowEffect).toBeDefined(); + + rainbowEffect = rainbowEffect!; + vi.spyOn(rainbowEffect, "apply"); game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase", false); - expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30); + expect(sereneGraceAttr.apply).toHaveBeenCalledOnce(); + expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true); + + expect(rainbowEffect.apply).toHaveBeenCalledTimes(0); } ); }); diff --git a/src/test/moves/shed_tail.test.ts b/src/test/moves/shed_tail.test.ts index c4df6c574cb..33a7d81e460 100644 --- a/src/test/moves/shed_tail.test.ts +++ b/src/test/moves/shed_tail.test.ts @@ -1,4 +1,5 @@ import { SubstituteTag } from "#app/data/battler-tags"; +import { MoveResult } from "#app/field/pokemon"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -53,4 +54,18 @@ describe("Moves - Shed Tail", () => { expect(substituteTag).toBeDefined(); expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4)); }); + + it("should fail if no ally is available to switch in", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const magikarp = game.scene.getPlayerPokemon()!; + expect(game.scene.getPlayerParty().length).toBe(1); + + game.move.select(Moves.SHED_TAIL); + + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(magikarp.isOnField()).toBeTruthy(); + expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); }); diff --git a/src/test/moves/sparkly_swirl.test.ts b/src/test/moves/sparkly_swirl.test.ts index 8449f2785f8..a83f1c3a437 100644 --- a/src/test/moves/sparkly_swirl.test.ts +++ b/src/test/moves/sparkly_swirl.test.ts @@ -38,7 +38,7 @@ describe("Moves - Sparkly Swirl", () => { .battleType("double") .statusEffect(StatusEffect.BURN); await game.classicMode.startBattle([ Species.RATTATA, Species.RATTATA, Species.RATTATA ]); - const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getParty(); + const [ leftPlayer, rightPlayer, partyPokemon ] = game.scene.getPlayerParty(); const leftOpp = game.scene.getEnemyPokemon()!; vi.spyOn(leftPlayer, "resetStatus"); diff --git a/src/test/moves/spikes.test.ts b/src/test/moves/spikes.test.ts index 1dd13f8f65e..35e89c8caf7 100644 --- a/src/test/moves/spikes.test.ts +++ b/src/test/moves/spikes.test.ts @@ -46,7 +46,7 @@ describe("Moves - Spikes", () => { game.doSwitchPokemon(1); await game.toNextTurn(); - const player = game.scene.getParty()[0]; + const player = game.scene.getPlayerParty()[0]; expect(player.hp).toBe(player.getMaxHp()); }, 20000); diff --git a/src/test/moves/tackle.test.ts b/src/test/moves/tackle.test.ts index 5d5ff1a366d..ff50f027f87 100644 --- a/src/test/moves/tackle.test.ts +++ b/src/test/moves/tackle.test.ts @@ -53,7 +53,7 @@ describe("Moves - Tackle", () => { Species.MIGHTYENA, ]); game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; - game.scene.getParty()[0].stats[Stat.ATK] = 50; + game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; diff --git a/src/test/moves/tera_starstorm.test.ts b/src/test/moves/tera_starstorm.test.ts index f0759dd242d..c7b6b66c1ce 100644 --- a/src/test/moves/tera_starstorm.test.ts +++ b/src/test/moves/tera_starstorm.test.ts @@ -70,8 +70,8 @@ describe("Moves - Tera Starstorm", () => { it("applies the effects when Terapagos in Stellar Form is fused with another Pokemon", async () => { await game.classicMode.startBattle([ Species.TERAPAGOS, Species.CHARMANDER, Species.MAGIKARP ]); - const fusionedMon = game.scene.getParty()[0]; - const magikarp = game.scene.getParty()[2]; + const fusionedMon = game.scene.getPlayerParty()[0]; + const magikarp = game.scene.getPlayerParty()[2]; // Fuse party members (taken from PlayerPokemon.fuse(...) function) fusionedMon.fusionSpecies = magikarp.species; diff --git a/src/test/moves/trick_or_treat.test.ts b/src/test/moves/trick_or_treat.test.ts new file mode 100644 index 00000000000..7ecd00ed076 --- /dev/null +++ b/src/test/moves/trick_or_treat.test.ts @@ -0,0 +1,47 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Trick Or Treat", () => { + 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.FORESTS_CURSE, Moves.TRICK_OR_TREAT ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("will replace added type from Forest's Curse", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + game.move.select(Moves.FORESTS_CURSE); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon!.summonData.addedType).toBe(Type.GRASS); + + game.move.select(Moves.TRICK_OR_TREAT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon?.summonData.addedType).toBe(Type.GHOST); + }); +}); diff --git a/src/test/moves/u_turn.test.ts b/src/test/moves/u_turn.test.ts index b995c20f503..c6e255e01b2 100644 --- a/src/test/moves/u_turn.test.ts +++ b/src/test/moves/u_turn.test.ts @@ -1,10 +1,8 @@ -import { Abilities } from "#app/enums/abilities"; -import { StatusEffect } from "#app/enums/status-effect"; -import { SwitchPhase } from "#app/phases/switch-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -38,19 +36,16 @@ describe("Moves - U-turn", () => { // arrange const playerHp = 1; game.override.ability(Abilities.REGENERATOR); - await game.startBattle([ - Species.RAICHU, - Species.SHUCKLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); game.scene.getPlayerPokemon()!.hp = playerHp; // act game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); // assert - expect(game.scene.getParty()[1].hp).toEqual(Math.floor(game.scene.getParty()[1].getMaxHp() * 0.33 + playerHp)); + expect(game.scene.getPlayerParty()[1].hp).toEqual(Math.floor(game.scene.getPlayerParty()[1].getMaxHp() * 0.33 + playerHp)); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE); }, 20000); @@ -58,15 +53,12 @@ describe("Moves - U-turn", () => { it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => { // arrange game.override.enemyAbility(Abilities.ROUGH_SKIN); - await game.startBattle([ - Species.RAICHU, - Species.SHUCKLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); // act game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(SwitchPhase, false); + await game.phaseInterceptor.to("SwitchPhase", false); // assert const playerPkm = game.scene.getPlayerPokemon()!; @@ -79,15 +71,12 @@ describe("Moves - U-turn", () => { it("triggers contact abilities on the u-turn user (eg poison point) before a new pokemon is switched in", async () => { // arrange game.override.enemyAbility(Abilities.POISON_POINT); - await game.startBattle([ - Species.RAICHU, - Species.SHUCKLE - ]); + await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); vi.spyOn(game.scene.getEnemyPokemon()!, "randSeedInt").mockReturnValue(0); // act game.move.select(Moves.U_TURN); - await game.phaseInterceptor.to(SwitchPhase, false); + await game.phaseInterceptor.to("SwitchPhase", false); // assert const playerPkm = game.scene.getPlayerPokemon()!; @@ -108,7 +97,7 @@ describe("Moves - U-turn", () => { // KO the opponent with U-Turn game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.isFainted()).toBe(true); // Check that U-Turn forced a switch diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index a72a9fbb5a3..61d8aaa9f5a 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -143,7 +143,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - for (const partyPokemon of scene.getParty()) { + for (const partyPokemon of scene.getPlayerParty()) { const pokemonId = partyPokemon.id; const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[]; @@ -230,13 +230,13 @@ describe("Absolute Avarice - Mystery Encounter", () => { it("should add Greedent to the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); - const partyCountBefore = scene.getParty().length; + const partyCountBefore = scene.getPlayerParty().length; await runMysteryEncounterToEnd(game, 3); - const partyCountAfter = scene.getParty().length; + const partyCountAfter = scene.getPlayerParty().length; expect(partyCountBefore + 1).toBe(partyCountAfter); - const greedent = scene.getParty()[scene.getParty().length - 1]; + const greedent = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; expect(greedent.species.speciesId).toBe(Species.GREEDENT); const moveset = greedent.moveset.map(m => m?.moveId); expect(moveset?.length).toBe(4); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index 0585b4ce72b..727a3993d9b 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -147,13 +147,13 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("Should remove the Pokemon from the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); - const initialPartySize = scene.getParty().length; + const initialPartySize = scene.getPlayerParty().length; const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; await runMysteryEncounterToEnd(game, 1); - expect(scene.getParty().length).toBe(initialPartySize - 1); - expect(scene.getParty().find(p => p.name === pokemonName)).toBeUndefined(); + expect(scene.getPlayerParty().length).toBe(initialPartySize - 1); + expect(scene.getPlayerParty().find(p => p.name === pokemonName)).toBeUndefined(); }); it("should leave encounter without battle", async () => { @@ -186,7 +186,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("should award EXP to a pokemon with an ability in EXTORTION_ABILITIES", async () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); - const party = scene.getParty(); + const party = scene.getPlayerParty(); const gyarados = party.find((pkm) => pkm.species.speciesId === Species.GYARADOS)!; const expBefore = gyarados.exp; @@ -199,7 +199,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { game.override.ability(Abilities.SYNCHRONIZE); // Not an extortion ability, so we can test extortion move await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, [ Species.ABRA ]); - const party = scene.getParty(); + const party = scene.getPlayerParty(); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; abra.moveset = [ new PokemonMove(Moves.BEAT_UP) ]; const expBefore = abra.exp; diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index bfa3d428bc0..2507b94e5ae 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -176,7 +176,7 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - scene.getParty().forEach(pkm => { + scene.getPlayerParty().forEach(pkm => { vi.spyOn(pkm, "getStat").mockReturnValue(1); // for ease return for every stat }); @@ -200,7 +200,7 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - scene.getParty().forEach(pkm => { + scene.getPlayerParty().forEach(pkm => { vi.spyOn(pkm, "getStat").mockReturnValue(1); // for ease return for every stat }); @@ -225,7 +225,7 @@ describe("Berries Abound - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - scene.getParty().forEach(pkm => { + scene.getPlayerParty().forEach(pkm => { vi.spyOn(pkm, "getStat").mockReturnValue(9999); // for ease return for every stat }); diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 82ed558e6db..7ea1f883bd1 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -218,7 +218,7 @@ describe("Clowning Around - Mystery Encounter", () => { // Stop next battle before it runs await game.phaseInterceptor.to(NewBattlePhase, false); - const leadPokemon = scene.getParty()[0]; + const leadPokemon = scene.getPlayerParty()[0]; expect(leadPokemon.customPokemonData?.ability).toBe(abilityToTrain); }); }); @@ -251,35 +251,35 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); // Set some moves on party for attack type booster generation - scene.getParty()[0].moveset = [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.THIEF) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.THIEF) ]; // 2 Sitrus Berries on lead scene.modifiers = []; let itemType = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); // 2 Ganlon Berries on lead itemType = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.GANLON ]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); // 5 Golden Punch on lead (ultra) itemType = generateModifierType(scene, modifierTypes.GOLDEN_PUNCH) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); // 5 Lucky Egg on lead (ultra) itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); // 5 Soul Dew on lead (rogue) itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); // 2 Golden Egg on lead (rogue) itemType = generateModifierType(scene, modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); // 5 Soul Dew on second party pokemon (these should not change) itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getParty()[1], 5, itemType); + await addItemToPokemon(scene, scene.getPlayerParty()[1], 5, itemType); await runMysteryEncounterToEnd(game, 2); - const leadItemsAfter = scene.getParty()[0].getHeldItems(); + const leadItemsAfter = scene.getPlayerParty()[0].getHeldItems(); const ultraCountAfter = leadItemsAfter .filter(m => m.type.tier === ModifierTier.ULTRA) .reduce((a, b) => a + b.stackCount, 0); @@ -289,7 +289,7 @@ describe("Clowning Around - Mystery Encounter", () => { expect(ultraCountAfter).toBe(10); expect(rogueCountAfter).toBe(7); - const secondItemsAfter = scene.getParty()[1].getHeldItems(); + const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems(); expect(secondItemsAfter.length).toBe(1); expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW"); expect(secondItemsAfter[0]?.stackCount).toBe(5); @@ -333,16 +333,16 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); // Same type moves on lead - scene.getParty()[0].moveset = [ new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF) ]; // Different type moves on second - scene.getParty()[1].moveset = [ new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL) ]; + scene.getPlayerParty()[1].moveset = [ new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL) ]; // No moves on third - scene.getParty()[2].moveset = []; + scene.getPlayerParty()[2].moveset = []; await runMysteryEncounterToEnd(game, 3); - const leadTypesAfter = scene.getParty()[0].customPokemonData?.types; - const secondaryTypesAfter = scene.getParty()[1].customPokemonData?.types; - const thirdTypesAfter = scene.getParty()[2].customPokemonData?.types; + const leadTypesAfter = scene.getPlayerParty()[0].customPokemonData?.types; + const secondaryTypesAfter = scene.getPlayerParty()[1].customPokemonData?.types; + const thirdTypesAfter = scene.getPlayerParty()[2].customPokemonData?.types; expect(leadTypesAfter.length).toBe(2); expect(leadTypesAfter[0]).toBe(Type.WATER); diff --git a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index 47625541160..147930f05d1 100644 --- a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -98,7 +98,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move - const partyLead = scene.getParty()[0]; + const partyLead = scene.getPlayerParty()[0]; partyLead.level = 1000; partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -119,7 +119,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should have a Baton in the rewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move - const partyLead = scene.getParty()[0]; + const partyLead = scene.getPlayerParty()[0]; partyLead.level = 1000; partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -157,7 +157,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene, "unshiftPhase"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getParty()[0].moveset = []; + scene.getPlayerParty()[0].moveset = []; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof LearnMovePhase).map(p => p[0]); @@ -169,7 +169,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getParty()[0].moveset = []; + scene.getPlayerParty()[0].moveset = []; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); @@ -196,13 +196,13 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should add Oricorio to the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - const partyCountBefore = scene.getParty().length; - scene.getParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; + const partyCountBefore = scene.getPlayerParty().length; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); - const partyCountAfter = scene.getParty().length; + const partyCountAfter = scene.getPlayerParty().length; expect(partyCountBefore + 1).toBe(partyCountAfter); - const oricorio = scene.getParty()[scene.getParty().length - 1]; + const oricorio = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; expect(oricorio.species.speciesId).toBe(Species.ORICORIO); const moveset = oricorio.moveset.map(m => m?.moveId); expect(moveset?.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy(); @@ -211,8 +211,8 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have a Dance type move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - const partyCountBefore = scene.getParty().length; - scene.getParty().forEach(p => p.moveset = []); + const partyCountBefore = scene.getPlayerParty().length; + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -223,7 +223,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { vi.spyOn(scene.ui, "playError"); await runSelectMysteryEncounterOption(game, 3); - const partyCountAfter = scene.getParty().length; + const partyCountAfter = scene.getPlayerParty().length; expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled @@ -236,7 +236,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.DRAGON_DANCE) ]; await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 66d628ef82f..c226d60a9b4 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -191,7 +191,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 2 Sitrus berries on party lead scene.modifiers = []; const sitrus = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ])!; - const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; await scene.addModifier(sitrusMod, true, false, false, true); await scene.updateModifiers(true); @@ -212,7 +212,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead scene.modifiers = []; const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -238,7 +238,7 @@ describe("Delibird-y - Mystery Encounter", () => { const sitrus = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ])!; // Sitrus berries on party - const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; await scene.addModifier(sitrusMod, true, false, false, true); await scene.updateModifiers(true); @@ -267,7 +267,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -291,7 +291,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]); + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -319,7 +319,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -353,7 +353,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 2 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 2; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -374,7 +374,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -400,7 +400,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -424,7 +424,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead scene.modifiers = []; const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getParty()[0]); + const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -453,7 +453,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 5a270f1cbec..92555e98679 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -175,7 +175,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const leadPokemonId = scene.getParty()?.[0].id; + const leadPokemonId = scene.getPlayerParty()?.[0].id; const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); @@ -202,7 +202,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should damage all non-fire party PKM by 20%, and burn + give Heatproof to a random Pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - const party = scene.getParty(); + const party = scene.getPlayerParty(); const lapras = party.find((pkm) => pkm.species.speciesId === Species.LAPRAS)!; lapras.status = new Status(StatusEffect.POISON); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; @@ -254,7 +254,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; + const leadPokemonItems = scene.getPlayerParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); expect(item).toBeDefined; }); diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index d23e7919267..a5098f682eb 100644 --- a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -151,7 +151,7 @@ describe("Fight or Flight - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have a Stealing move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); - scene.getParty().forEach(p => p.moveset = []); + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -175,7 +175,7 @@ describe("Fight or Flight - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); // Mock moveset - scene.getParty()[0].moveset = [ new PokemonMove(Moves.KNOCK_OFF) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.KNOCK_OFF) ]; const item = game.scene.currentBattle.mysteryEncounter!.misc; await runMysteryEncounterToEnd(game, 2); diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index b08f8008b68..e8d19ff50b9 100644 --- a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -106,10 +106,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for the first of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[0].species.speciesId; + const speciesBefore = scene.getPlayerParty()[0].species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -119,10 +119,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for the second of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[1].species.speciesId; + const speciesBefore = scene.getPlayerParty()[1].species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2, optionNo: 2 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -132,10 +132,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for the third of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[2].species.speciesId; + const speciesBefore = scene.getPlayerParty()[2].species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 3, optionNo: 3 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -166,10 +166,10 @@ describe("Global Trade System - Mystery Encounter", () => { it("Should trade a Pokemon from the player's party for a random wonder trade Pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getParty()[2].species.speciesId; + const speciesBefore = scene.getPlayerParty()[2].species.speciesId; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); - const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); @@ -204,7 +204,7 @@ describe("Global Trade System - Mystery Encounter", () => { // Set 2 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 2; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -229,7 +229,7 @@ describe("Global Trade System - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; modifier.stackCount = 1; await scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index dec14d46cc8..51f759c9268 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -114,7 +114,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const laprasSpecies = getPokemonSpecies(Species.LAPRAS); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const blastoise = party.find((pkm) => pkm.species.speciesId === Species.BLASTOISE); const expBefore = blastoise!.exp; @@ -179,7 +179,7 @@ describe("Lost at Sea - Mystery Encounter", () => { game.override.startingWave(wave); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const pidgeot = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); const expBefore = pidgeot!.exp; @@ -241,7 +241,7 @@ describe("Lost at Sea - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - const party = game.scene.getParty(); + const party = game.scene.getPlayerParty(); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); diff --git a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts index ba8ce648a3f..e063a4f3349 100644 --- a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -101,7 +101,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.calculateStats(); }); @@ -109,7 +109,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[0].moveset; + const moves = scene.getPlayerParty()[0].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -120,7 +120,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.ivs = [ 20, 20, 20, 20, 20, 20 ]; p.calculateStats(); @@ -129,7 +129,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[1].moveset; + const moves = scene.getPlayerParty()[1].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -166,7 +166,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.calculateStats(); }); @@ -174,7 +174,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[2].moveset; + const moves = scene.getPlayerParty()[2].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -185,7 +185,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Override party levels to 50 so stats can be fully reflective - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.level = 50; p.ivs = [ 20, 20, 20, 20, 20, 20 ]; p.calculateStats(); @@ -194,7 +194,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[3].moveset; + const moves = scene.getPlayerParty()[3].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -232,7 +232,7 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Mock movesets - scene.getParty().forEach(p => p.moveset = []); + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -256,12 +256,12 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Mock moveset - scene.getParty()[0].moveset = [ new PokemonMove(Moves.ATTRACT) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.ATTRACT) ]; await runMysteryEncounterToEnd(game, 3); expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(2.5), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getParty()[0].moveset; + const moves = scene.getPlayerParty()[0].moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } diff --git a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts index a3a43815ec6..7fc2490fcc9 100644 --- a/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -152,7 +152,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - expect(scene.getParty().length).toBe(1); + expect(scene.getPlayerParty().length).toBe(1); }); it("Should reward the player with friendship and eggs based on pokemon selected", async () => { @@ -231,7 +231,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - expect(scene.getParty().length).toBe(1); + expect(scene.getPlayerParty().length).toBe(1); }); it("Should reward the player with friendship and eggs based on pokemon selected", async () => { @@ -310,7 +310,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); - expect(scene.getParty().length).toBe(1); + expect(scene.getPlayerParty().length).toBe(1); }); it("Should reward the player with friendship and eggs based on pokemon selected", async () => { diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 040381c4ac3..a50c0cf4c9e 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -140,14 +140,14 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { scene.money = 20000; await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); - const initialPartySize = scene.getParty().length; + const initialPartySize = scene.getPlayerParty().length; const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; await runMysteryEncounterToEnd(game, 1); - expect(scene.getParty().length).toBe(initialPartySize + 1); + expect(scene.getPlayerParty().length).toBe(initialPartySize + 1); - const newlyPurchasedPokemon = scene.getParty()[scene.getParty().length - 1]; + const newlyPurchasedPokemon = scene.getPlayerParty()[scene.getPlayerParty().length - 1]; expect(newlyPurchasedPokemon.name).toBe(pokemonName); expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy(); }); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index 4d95ff31d36..be0e6e68b5e 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -143,10 +143,10 @@ describe("The Strong Stuff - Mystery Encounter", () => { it("should lower stats of 2 highest BST and raise stats for rest of party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal()); + const bstsPrior = scene.getPlayerParty().map(p => p.getSpeciesForm().getBaseStatTotal()); await runMysteryEncounterToEnd(game, 1); - const bstsAfter = scene.getParty().map(p => { + const bstsAfter = scene.getPlayerParty().map(p => { const baseStats = p.getSpeciesForm().baseStats.slice(0); scene.applyModifiers(PokemonBaseStatTotalModifier, true, p, baseStats); return baseStats.reduce((a, b) => a + b); diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index fa1b5ecdeb7..c811bda673d 100644 --- a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -214,11 +214,11 @@ describe("Uncommon Breed - Mystery Encounter", () => { // Berries on party lead const sitrus = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.SITRUS ])!; - const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; sitrusMod.stackCount = 2; await scene.addModifier(sitrusMod, true, false, false, true); const ganlon = generateModifierType(scene, modifierTypes.BERRY, [ BerryType.GANLON ])!; - const ganlonMod = ganlon.newModifier(scene.getParty()[0]) as BerryModifier; + const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; ganlonMod.stackCount = 3; await scene.addModifier(ganlonMod, true, false, false, true); await scene.updateModifiers(true); @@ -248,7 +248,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have an Attracting move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); - scene.getParty().forEach(p => p.moveset = []); + scene.getPlayerParty().forEach(p => p.moveset = []); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); @@ -270,7 +270,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); // Mock moveset - scene.getParty()[0].moveset = [ new PokemonMove(Moves.CHARM) ]; + scene.getPlayerParty()[0].moveset = [ new PokemonMove(Moves.CHARM) ]; await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index c1fa6d83a18..ec9cf1509b1 100644 --- a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -110,14 +110,14 @@ describe("Weird Dream - Mystery Encounter", () => { it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - const pokemonPrior = scene.getParty().map(pokemon => pokemon); + const pokemonPrior = scene.getPlayerParty().map(pokemon => pokemon); const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal()); await runMysteryEncounterToEnd(game, 1); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const pokemonAfter = scene.getParty(); + const pokemonAfter = scene.getPlayerParty(); const bstsAfter = pokemonAfter.map(pokemon => pokemon.getSpeciesForm().getBaseStatTotal()); const bstDiff = bstsAfter.map((bst, index) => bst - bstsPrior[index]); @@ -184,7 +184,7 @@ describe("Weird Dream - Mystery Encounter", () => { const enemyField = scene.getEnemyField(); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(enemyField.length).toBe(1); - expect(scene.getEnemyParty().length).toBe(scene.getParty().length); + expect(scene.getEnemyParty().length).toBe(scene.getPlayerParty().length); }); it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => { @@ -227,14 +227,14 @@ describe("Weird Dream - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - const levelsPrior = scene.getParty().map(p => p.level); + const levelsPrior = scene.getPlayerParty().map(p => p.level); await runMysteryEncounterToEnd(game, 3); - const levelsAfter = scene.getParty().map(p => p.level); + const levelsAfter = scene.getPlayerParty().map(p => p.level); for (let i = 0; i < levelsPrior.length; i++) { expect(Math.max(Math.ceil(0.9 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); - expect(scene.getParty()[i].levelExp).toBe(0); + expect(scene.getPlayerParty()[i].levelExp).toBe(0); } expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index 134966a188d..b57a88d5cad 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -50,7 +50,7 @@ describe("Mystery Encounter Utils", () => { it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { // Both pokemon fainted - scene.getParty().forEach(p => { + scene.getPlayerParty().forEach(p => { p.hp = 0; p.trySetStatus(StatusEffect.FAINT); p.updateInfo(); @@ -70,7 +70,7 @@ describe("Mystery Encounter Utils", () => { it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => { // Only faint 1st pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); party[0].updateInfo(); @@ -89,7 +89,7 @@ describe("Mystery Encounter Utils", () => { it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { // Only faint 1st pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); party[0].updateInfo(); @@ -108,7 +108,7 @@ describe("Mystery Encounter Utils", () => { it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { // Only faint 1st pokemon - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); party[0].updateInfo(); @@ -128,7 +128,7 @@ describe("Mystery Encounter Utils", () => { describe("getHighestLevelPlayerPokemon", () => { it("gets highest level pokemon", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; const result = getHighestLevelPlayerPokemon(scene); @@ -136,7 +136,7 @@ describe("Mystery Encounter Utils", () => { }); it("gets highest level pokemon at different index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[1].level = 100; const result = getHighestLevelPlayerPokemon(scene); @@ -144,7 +144,7 @@ describe("Mystery Encounter Utils", () => { }); it("breaks ties by getting returning lower index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; party[1].level = 100; @@ -153,7 +153,7 @@ describe("Mystery Encounter Utils", () => { }); it("returns highest level unfainted if unfainted is true", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); @@ -167,7 +167,7 @@ describe("Mystery Encounter Utils", () => { describe("getLowestLevelPokemon", () => { it("gets lowest level pokemon", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; const result = getLowestLevelPlayerPokemon(scene); @@ -175,7 +175,7 @@ describe("Mystery Encounter Utils", () => { }); it("gets lowest level pokemon at different index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[1].level = 100; const result = getLowestLevelPlayerPokemon(scene); @@ -183,7 +183,7 @@ describe("Mystery Encounter Utils", () => { }); it("breaks ties by getting returning lower index", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 100; party[1].level = 100; @@ -192,7 +192,7 @@ describe("Mystery Encounter Utils", () => { }); it("returns lowest level unfainted if unfainted is true", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); party[0].level = 10; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); @@ -239,7 +239,7 @@ describe("Mystery Encounter Utils", () => { describe("koPlayerPokemon", () => { it("KOs a pokemon", () => { - const party = scene.getParty(); + const party = scene.getPlayerParty(); const arceus = party[0]; arceus.hp = 100; expect(arceus.isAllowedInBattle()).toBe(true); diff --git a/src/test/phases/frenzy-move-reset.test.ts b/src/test/phases/frenzy-move-reset.test.ts new file mode 100644 index 00000000000..db9ec2bfe66 --- /dev/null +++ b/src/test/phases/frenzy-move-reset.test.ts @@ -0,0 +1,72 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#enums/status-effect"; +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, it, expect } from "vitest"; + +describe("Frenzy Move Reset", () => { + 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 + .battleType("single") + .disableCrits() + .starterSpecies(Species.MAGIKARP) + .moveset(Moves.THRASH) + .statusEffect(StatusEffect.PARALYSIS) + .enemyMoveset(Moves.SPLASH) + .enemyLevel(100) + .enemySpecies(Species.SHUCKLE) + .enemyAbility(Abilities.BALL_FETCH); + }); + + /* + * Thrash (or frenzy moves in general) should not continue to run if attack fails due to paralysis + * + * This is a 3-turn Thrash test: + * 1. Thrash is selected and succeeds to hit the enemy -> Enemy Faints + * + * 2. Thrash is automatically selected but misses due to paralysis + * Note: After missing the Pokemon should stop automatically attacking + * + * 3. At the start of the 3rd turn the Player should be able to select a move/switch Pokemon/etc. + * Note: This means that BattlerTag.FRENZY is not anymore in pokemon.summonData.tags and pokemon.summonData.moveQueue is empty + * + */ + it("should cancel frenzy move if move fails turn 2", async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.THRASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceStatusActivation(false); + await game.toNextTurn(); + + expect(playerPokemon.summonData.moveQueue.length).toBe(2); + expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(true); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceStatusActivation(true); + await game.toNextTurn(); + + expect(playerPokemon.summonData.moveQueue.length).toBe(0); + expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(false); + }); +}); diff --git a/src/test/phases/select-modifier-phase.test.ts b/src/test/phases/select-modifier-phase.test.ts index a945aff055b..60f81f3ad54 100644 --- a/src/test/phases/select-modifier-phase.test.ts +++ b/src/test/phases/select-modifier-phase.test.ts @@ -150,10 +150,10 @@ describe("SelectModifierPhase", () => { const pokemon = new PlayerPokemon(scene, getPokemonSpecies(Species.BULBASAUR), 10, undefined, 0, undefined, true, 2, undefined, undefined, undefined); // Fill party with max shinies - while (scene.getParty().length > 0) { - scene.getParty().pop(); + while (scene.getPlayerParty().length > 0) { + scene.getPlayerParty().pop(); } - scene.getParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); + scene.getPlayerParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); scene.pushPhase(selectModifierPhase); diff --git a/src/test/ui/starter-select.test.ts b/src/test/ui/starter-select.test.ts index 94370ca1b74..8f86d62c546 100644 --- a/src/test/ui/starter-select.test.ts +++ b/src/test/ui/starter-select.test.ts @@ -91,10 +91,10 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].gender).toBe(Gender.MALE); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.MALE); }, 20000); it("Bulbasaur - shiny - variant 2 female hardy overgrow", async() => { @@ -152,11 +152,11 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].nature).toBe(Nature.HARDY); - expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.OVERGROW); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.HARDY); + expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(Abilities.OVERGROW); }, 20000); it("Bulbasaur - shiny - variant 2 female lonely chlorophyl", async() => { @@ -216,12 +216,12 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].gender).toBe(Gender.FEMALE); - expect(game.scene.getParty()[0].nature).toBe(Nature.LONELY); - expect(game.scene.getParty()[0].getAbility().id).toBe(Abilities.CHLOROPHYLL); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); + expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.LONELY); + expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(Abilities.CHLOROPHYLL); }, 20000); it("Bulbasaur - shiny - variant 2 female", async() => { @@ -279,10 +279,10 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(2); - expect(game.scene.getParty()[0].gender).toBe(Gender.FEMALE); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(2); + expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); }, 20000); it("Bulbasaur - not shiny", async() => { @@ -340,9 +340,9 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(false); - expect(game.scene.getParty()[0].variant).toBe(0); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(false); + expect(game.scene.getPlayerParty()[0].variant).toBe(0); }, 20000); it("Bulbasaur - shiny - variant 1", async() => { @@ -401,9 +401,9 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(1); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(1); }, 20000); it("Bulbasaur - shiny - variant 0", async() => { @@ -461,9 +461,9 @@ describe("UI - Starter select", () => { }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR); - expect(game.scene.getParty()[0].shiny).toBe(true); - expect(game.scene.getParty()[0].variant).toBe(0); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.BULBASAUR); + expect(game.scene.getPlayerParty()[0].shiny).toBe(true); + expect(game.scene.getPlayerParty()[0].variant).toBe(0); }, 20000); it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column", async() => { @@ -527,7 +527,7 @@ describe("UI - Starter select", () => { saveSlotSelectUiHandler.processInput(Button.ACTION); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CATERPIE); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.CATERPIE); }, 20000); it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1)", async() => { @@ -593,6 +593,6 @@ describe("UI - Starter select", () => { saveSlotSelectUiHandler.processInput(Button.ACTION); }); await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.NIDORAN_M); + expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.NIDORAN_M); }, 20000); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 86c51972c8b..a7d67911228 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -1,20 +1,20 @@ import { updateUserInfo } from "#app/account"; import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; -import { BattleStyle } from "#app/enums/battle-style"; -import { Moves } from "#app/enums/moves"; import { getMoveTargets } from "#app/data/move"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import Trainer from "#app/field/trainer"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import overrides from "#app/overrides"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { FaintPhase } from "#app/phases/faint-phase"; import { LoginPhase } from "#app/phases/login-phase"; import { MovePhase } from "#app/phases/move-phase"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import { SelectTargetPhase } from "#app/phases/select-target-phase"; @@ -24,37 +24,36 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; import ErrorInterceptor from "#app/test/utils/errorInterceptor"; import InputsHandler from "#app/test/utils/inputsHandler"; +import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; import CommandUiHandler from "#app/ui/command-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler from "#app/ui/party-ui-handler"; import TargetSelectUiHandler from "#app/ui/target-select-ui-handler"; import { Mode } from "#app/ui/ui"; +import { isNullOrUndefined } from "#app/utils"; +import { BattleStyle } from "#enums/battle-style"; import { Button } from "#enums/buttons"; +import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpNotification } from "#enums/exp-notification"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils"; import GameWrapper from "#test/utils/gameWrapper"; +import { ChallengeModeHelper } from "#test/utils/helpers/challengeModeHelper"; +import { ClassicModeHelper } from "#test/utils/helpers/classicModeHelper"; +import { DailyModeHelper } from "#test/utils/helpers/dailyModeHelper"; +import { ModifierHelper } from "#test/utils/helpers/modifiersHelper"; +import { MoveHelper } from "#test/utils/helpers/moveHelper"; +import { OverridesHelper } from "#test/utils/helpers/overridesHelper"; +import { ReloadHelper } from "#test/utils/helpers/reloadHelper"; +import { SettingsHelper } from "#test/utils/helpers/settingsHelper"; import PhaseInterceptor from "#test/utils/phaseInterceptor"; import TextInterceptor from "#test/utils/TextInterceptor"; import { AES, enc } from "crypto-js"; import fs from "fs"; -import { vi } from "vitest"; -import { ClassicModeHelper } from "./helpers/classicModeHelper"; -import { DailyModeHelper } from "./helpers/dailyModeHelper"; -import { ChallengeModeHelper } from "./helpers/challengeModeHelper"; -import { MoveHelper } from "./helpers/moveHelper"; -import { OverridesHelper } from "./helpers/overridesHelper"; -import { SettingsHelper } from "./helpers/settingsHelper"; -import { ReloadHelper } from "./helpers/reloadHelper"; -import { ModifierHelper } from "./helpers/modifiersHelper"; -import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; -import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; -import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; -import { expect } from "vitest"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { isNullOrUndefined } from "#app/utils"; -import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; +import { expect, vi } from "vitest"; /** * Class to manage the game state and transitions between phases. @@ -427,7 +426,7 @@ export default class GameManager { * @param pokemonIndex the index of the pokemon in your party to revive */ doRevivePokemon(pokemonIndex: number) { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const candidate = new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0); const modifier = candidate.type!.newModifier(party[pokemonIndex]); this.scene.addModifier(modifier, false); diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 543ee9627fe..0c70bcf7f18 100644 --- a/src/test/utils/gameManagerUtils.ts +++ b/src/test/utils/gameManagerUtils.ts @@ -105,7 +105,7 @@ export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: Spe const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = scene.addPlayerPokemon(starter.species, scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); - scene.getParty().push(starterPokemon); + scene.getPlayerParty().push(starterPokemon); }); scene.currentBattle = new Battle(getGameMode(GameModes.CLASSIC), 5, BattleType.WILD, undefined, false); diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 48c0007118b..22517502a05 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -24,6 +24,7 @@ import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin; import EventEmitter = Phaser.Events.EventEmitter; import UpdateList = Phaser.GameObjects.UpdateList; import { version } from "../../../package.json"; +import { MockTimedEventManager } from "./mocks/mockTimedEventManager"; Object.defineProperty(window, "localStorage", { value: mockLocalStorage(), @@ -232,6 +233,7 @@ export default class GameWrapper { this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.time = new MockClock(this.scene); this.scene.remove = vi.fn(); // TODO: this should be stubbed differently + this.scene.eventManager = new MockTimedEventManager(); // Disable Timed Events } } diff --git a/src/test/utils/mocks/mockTimedEventManager.ts b/src/test/utils/mocks/mockTimedEventManager.ts new file mode 100644 index 00000000000..b44729996a7 --- /dev/null +++ b/src/test/utils/mocks/mockTimedEventManager.ts @@ -0,0 +1,17 @@ +import { TimedEventManager } from "#app/timed-event-manager"; + +/** Mock TimedEventManager so that ongoing events don't impact tests */ +export class MockTimedEventManager extends TimedEventManager { + override activeEvent() { + return undefined; + } + override isEventActive(): boolean { + return false; + } + override getFriendshipMultiplier(): number { + return 1; + } + override getShinyMultiplier(): number { + return 1; + } +} diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 4c56ada1c94..3b2b3619397 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -35,7 +35,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2024, 10, 4, 0)), bannerKey: "halloween2024-event-", scale: 0.21, - availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ] + availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ] } ]; diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 832d665b290..180fc66ed9b 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -170,7 +170,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler { if (!this.scene.showLevelUpStats) { return resolve(); } - const newStats = (this.scene as BattleScene).getParty()[partyMemberIndex].stats; + const newStats = (this.scene as BattleScene).getPlayerParty()[partyMemberIndex].stats; let levelUpStatsValuesText = ""; for (const s of PERMANENT_STATS) { levelUpStatsValuesText += `${showTotals ? newStats[s] : newStats[s] - prevStats[s]}\n`; diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 8f977ba2ac0..b14f5381a84 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -107,7 +107,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { let pokemonIconX = -20; let pokemonIconY = 6; - if ([ "de", "es", "fr", "ko", "pt-BR" ].includes(currentLanguage)) { + if ([ "de", "es-ES", "fr", "ko", "pt-BR" ].includes(currentLanguage)) { gachaTextStyle = TextStyle.SMALLER_WINDOW_ALT; gachaX = 2; gachaY = 2; @@ -115,7 +115,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { let legendaryLabelX = gachaX; let legendaryLabelY = gachaY; - if ([ "de", "es" ].includes(currentLanguage)) { + if ([ "de", "es-ES" ].includes(currentLanguage)) { pokemonIconX = -25; pokemonIconY = 10; legendaryLabelX = -6; @@ -128,7 +128,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { switch (gachaType as GachaType) { case GachaType.LEGENDARY: - if ([ "de", "es" ].includes(currentLanguage)) { + if ([ "de", "es-ES" ].includes(currentLanguage)) { gachaUpLabel.setAlign("center"); gachaUpLabel.setY(0); } @@ -149,7 +149,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { gachaInfoContainer.add(pokemonIcon); break; case GachaType.MOVE: - if ([ "de", "es", "fr", "pt-BR" ].includes(currentLanguage)) { + if ([ "de", "es-ES", "fr", "pt-BR" ].includes(currentLanguage)) { gachaUpLabel.setAlign("center"); gachaUpLabel.setY(0); } diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 301d54daa3a..fea0a70af91 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -32,7 +32,7 @@ let wikiUrl = "https://wiki.pokerogue.net/start"; const discordUrl = "https://discord.gg/uWpTfdKG49"; const githubUrl = "https://github.com/pagefaultgames/pokerogue"; const redditUrl = "https://www.reddit.com/r/pokerogue"; -const donateUrl = "https://github.com/sponsors/patapancakes"; +const donateUrl = "https://github.com/sponsors/pagefaultgames"; export default class MenuUiHandler extends MessageUiHandler { private readonly textPadding = 8; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index e96fde8d54f..a26aa572ef3 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -346,13 +346,13 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.optionsMode) { const option = this.options[this.optionsCursor]; if (button === Button.ACTION) { - const pokemon = this.scene.getParty()[this.cursor]; + const pokemon = this.scene.getPlayerParty()[this.cursor]; if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode && option !== PartyOption.CANCEL) { this.startTransfer(); let ableToTransfer: string; - for (let p = 0; p < this.scene.getParty().length; p++) { // this fore look goes through each of the party pokemon - const newPokemon = this.scene.getParty()[p]; + for (let p = 0; p < this.scene.getPlayerParty().length; p++) { // this fore look goes through each of the party pokemon + const newPokemon = this.scene.getPlayerParty()[p]; // this next line gets all of the transferable items from pokemon [p]; it does this by getting all the held modifiers that are transferable and checking to see if they belong to pokemon [p] const getTransferrableItemsFromPokemon = (newPokemon: PlayerPokemon) => this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; @@ -409,7 +409,7 @@ export default class PartyUiHandler extends MessageUiHandler { filterResult = this.moveSelectFilter(pokemon.moveset[this.optionsCursor]!); // TODO: is this bang correct? } } else { - filterResult = (this.selectFilter as PokemonModifierTransferSelectFilter)(pokemon, getTransferrableItemsFromPokemon(this.scene.getParty()[this.transferCursor])[this.transferOptionCursor]); + filterResult = (this.selectFilter as PokemonModifierTransferSelectFilter)(pokemon, getTransferrableItemsFromPokemon(this.scene.getPlayerParty()[this.transferCursor])[this.transferOptionCursor]); } if (filterResult === null) { if (this.partyUiMode !== PartyUiMode.SPLICE) { @@ -419,7 +419,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (option === PartyOption.TRANSFER) { if (this.transferCursor !== this.cursor) { if (this.transferAll) { - getTransferrableItemsFromPokemon(this.scene.getParty()[this.transferCursor]).forEach((_, i) => (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, i, this.transferQuantitiesMax[i], this.cursor)); + getTransferrableItemsFromPokemon(this.scene.getPlayerParty()[this.transferCursor]).forEach((_, i) => (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, i, this.transferQuantitiesMax[i], this.cursor)); } else { (this.selectCallback as PartyModifierTransferSelectCallback)(this.transferCursor, this.transferOptionCursor, this.transferQuantities[this.transferOptionCursor], this.cursor); } @@ -580,7 +580,7 @@ export default class PartyUiHandler extends MessageUiHandler { // show move description if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { const option = this.options[this.optionsCursor]; - const pokemon = this.scene.getParty()[this.cursor]; + const pokemon = this.scene.getPlayerParty()[this.cursor]; const move = allMoves[pokemon.getLearnableLevelMoves()[option]]; if (move) { this.moveInfoOverlay.show(move); @@ -596,7 +596,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) { /** Initialize item quantities for the selected Pokemon */ const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === this.scene.getPlayerParty()[this.cursor].id) as PokemonHeldItemModifier[]; this.transferQuantities = itemModifiers.map(item => item.getStackCount()); this.transferQuantitiesMax = itemModifiers.map(item => item.getStackCount()); } @@ -664,7 +664,7 @@ export default class PartyUiHandler extends MessageUiHandler { } populatePartySlots() { - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); if (this.cursor < 6 && this.cursor >= party.length) { this.cursor = party.length - 1; @@ -803,7 +803,7 @@ export default class PartyUiHandler extends MessageUiHandler { } updateOptions(): void { - const pokemon = this.scene.getParty()[this.cursor]; + const pokemon = this.scene.getPlayerParty()[this.cursor]; const learnableLevelMoves = this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER ? pokemon.getLearnableLevelMoves() @@ -1059,13 +1059,13 @@ export default class PartyUiHandler extends MessageUiHandler { } doRelease(slotIndex: integer): void { - this.showText(this.getReleaseMessage(getPokemonNameWithAffix(this.scene.getParty()[slotIndex])), null, () => { + this.showText(this.getReleaseMessage(getPokemonNameWithAffix(this.scene.getPlayerParty()[slotIndex])), null, () => { this.clearPartySlots(); this.scene.removePartyMemberModifiers(slotIndex); - const releasedPokemon = this.scene.getParty().splice(slotIndex, 1)[0]; + const releasedPokemon = this.scene.getPlayerParty().splice(slotIndex, 1)[0]; releasedPokemon.destroy(); this.populatePartySlots(); - if (this.cursor >= this.scene.getParty().length) { + if (this.cursor >= this.scene.getPlayerParty().length) { this.setCursor(this.cursor - 1); } if (this.partyUiMode === PartyUiMode.RELEASE) { diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 5c3a22639dd..5b11aff43b1 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -21,24 +21,6 @@ interface LanguageSetting { } const languageSettings: { [key: string]: LanguageSetting } = { - "en": { - infoContainerTextSize: "64px" - }, - "de": { - infoContainerTextSize: "64px", - }, - "es": { - infoContainerTextSize: "64px" - }, - "fr": { - infoContainerTextSize: "64px" - }, - "it": { - infoContainerTextSize: "64px" - }, - "zh": { - infoContainerTextSize: "64px" - }, "pt": { infoContainerTextSize: "60px", infoContainerLabelXPos: -15, @@ -237,14 +219,20 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { const formKey = (pokemon.species?.forms?.[pokemon.formIndex!]?.formKey); const formText = Utils.capitalizeString(formKey, "-", false, false) || ""; - const speciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false); + const speciesName = Utils.capitalizeString(Species[pokemon.species.speciesId], "_", true, false); let formName = ""; if (pokemon.species.speciesId === Species.ARCEUS) { formName = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`); } else { const i18key = `pokemonForm:${speciesName}${formText}`; - formName = i18next.exists(i18key) ? i18next.t(i18key) : formText; + if (i18next.exists(i18key)) { + formName = i18next.t(i18key); + } else { + const rootSpeciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false); + const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`; + formName = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText; + } } if (formName) { diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts index fc9eb85cbaf..2c35ff8ee7f 100644 --- a/src/ui/registration-form-ui-handler.ts +++ b/src/ui/registration-form-ui-handler.ts @@ -13,7 +13,7 @@ interface LanguageSetting { } const languageSettings: { [key: string]: LanguageSetting } = { - "es":{ + "es-ES": { inputFieldFontSize: "50px", errorMessageFontSize: "40px", } diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 0ca47241136..4975f05b8a3 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -674,7 +674,7 @@ export default class RunInfoUiHandler extends UiHandler { const def = i18next.t("pokemonInfo:Stat.DEFshortened") + ": " + pStats[2]; const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened") + ": " + pStats[3]; const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened") + ": " + pStats[4]; - const speedLabel = (currentLanguage === "es" || currentLanguage === "pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); + const speedLabel = (currentLanguage === "es-ES" || currentLanguage === "pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); const speed = speedLabel + ": " + pStats[5]; // Column 1: HP Atk Def const pokeStatText1 = addBBCodeTextObject(this.scene, -5, 0, hp, TextStyle.SUMMARY, { fontSize: textContainerFontSize, lineSpacing: lineSpacing }); diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 7bfecb1f99b..b36c0af6ec8 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -132,7 +132,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler { }, () => { ui.revertMode(); ui.showText("", 0); - }, false, 0, 19, 2000); + }, false, 0, 19, import.meta.env.DEV ? 300 : 2000); }); } else if (this.sessionSlots[cursor].hasData === false) { saveAndCallback(); diff --git a/src/ui/settings/settings-display-ui-handler.ts b/src/ui/settings/settings-display-ui-handler.ts index a25dbf87b7d..c4cbb0dfe58 100644 --- a/src/ui/settings/settings-display-ui-handler.ts +++ b/src/ui/settings/settings-display-ui-handler.ts @@ -29,10 +29,10 @@ export default class SettingsDisplayUiHandler extends AbstractSettingsUiHandler label: "English", }; break; - case "es": + case "es-ES": this.settings[languageIndex].options[0] = { - value: "Español", - label: "Español", + value: "Español (ES)", + label: "Español (ES)", }; break; case "it": diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index bb999dc736a..3a29f9431e7 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -20,7 +20,6 @@ import { Type } from "#app/data/type"; import { GameModes } from "#app/game-mode"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "#app/system/game-data"; import { Tutorial, handleTutorial } from "#app/tutorial"; -import * as Utils from "#app/utils"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; @@ -50,6 +49,7 @@ import { EncounterPhase } from "#app/phases/encounter-phase"; import { TitlePhase } from "#app/phases/title-phase"; import { Abilities } from "#enums/abilities"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; +import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -81,7 +81,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { instructionTextSize: "35px", starterInfoXPos: 33, }, - "es":{ + "es-ES":{ starterInfoTextSize: "56px", instructionTextSize: "35px", }, @@ -197,6 +197,15 @@ function findClosestStarterRow(index: number, numberOfRows: number) { return closestRowIndex; } +interface SpeciesDetails { + shiny?: boolean, + formIndex?: integer + female?: boolean, + variant?: Variant, + abilityIndex?: integer, + natureIndex?: integer, + forSeen?: boolean, // default = false +} export default class StarterSelectUiHandler extends MessageUiHandler { private starterSelectContainer: Phaser.GameObjects.Container; @@ -298,10 +307,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private canCycleAbility: boolean; private canCycleNature: boolean; private canCycleVariant: boolean; - private value: integer = 0; - private canAddParty: boolean; - private assetLoadCancelled: Utils.BooleanHolder | null; + private assetLoadCancelled: BooleanHolder | null; public cursorObj: Phaser.GameObjects.Image; private starterCursorObjs: Phaser.GameObjects.Image[]; private pokerusCursorObjs: Phaser.GameObjects.Image[]; @@ -387,7 +394,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (index === 0 || index === 19) { return; } - const typeSprite = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("types")); + const typeSprite = this.scene.add.sprite(0, 0, getLocalizedSpriteKey("types")); typeSprite.setScale(0.5); typeSprite.setFrame(type.toLowerCase()); typeOptions.push(new DropDownOption(this.scene, index, new DropDownLabel("", typeSprite))); @@ -661,12 +668,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); this.starterSelectContainer.add(this.pokemonSprite); - this.type1Icon = this.scene.add.sprite(8, 98, Utils.getLocalizedSpriteKey("types")); + this.type1Icon = this.scene.add.sprite(8, 98, getLocalizedSpriteKey("types")); this.type1Icon.setScale(0.5); this.type1Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type1Icon); - this.type2Icon = this.scene.add.sprite(26, 98, Utils.getLocalizedSpriteKey("types")); + this.type2Icon = this.scene.add.sprite(26, 98, getLocalizedSpriteKey("types")); this.type2Icon.setScale(0.5); this.type2Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type2Icon); @@ -1133,20 +1140,20 @@ export default class StarterSelectUiHandler extends MessageUiHandler { targets: icon, loop: -1, // Make the initial bounce a little randomly delayed - delay: Utils.randIntRange(0, 50) * 5, + delay: randIntRange(0, 50) * 5, loopDelay: 1000, tweens: [ { targets: icon, y: 2 - 5, - duration: Utils.fixedInt(125), + duration: fixedInt(125), ease: "Cubic.easeOut", yoyo: true }, { targets: icon, y: 2 - 3, - duration: Utils.fixedInt(150), + duration: fixedInt(150), ease: "Cubic.easeOut", yoyo: true } @@ -1442,7 +1449,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const [ isDupe, removeIndex ]: [boolean, number] = this.isInParty(this.lastSpecies); // checks to see if the pokemon is a duplicate; if it is, returns the index that will be removed const isPartyValid = this.isPartyValid(); - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, this.lastSpecies, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId)), isPartyValid); @@ -1593,11 +1600,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (!starterAttributes) { starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {}; } - starterAttributes.nature = n as unknown as integer; + starterAttributes.nature = n; this.clearText(); ui.setMode(Mode.STARTER_SELECT); // set nature for starter - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, n, undefined); + this.setSpeciesDetails(this.lastSpecies, { natureIndex: n }); this.blockInput = false; return true; } @@ -1635,7 +1642,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { handler: () => { starterData.passiveAttr |= PassiveAttr.ENABLED; ui.setMode(Mode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies); return true; } }); @@ -1645,7 +1652,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { handler: () => { starterData.passiveAttr ^= PassiveAttr.ENABLED; ui.setMode(Mode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies); return true; } }); @@ -1864,7 +1871,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { // Change to shiny, we need to get the proper default variant const newProps = this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId)); const newVariant = starterAttributes.variant ? starterAttributes.variant as Variant : newProps.variant; - this.setSpeciesDetails(this.lastSpecies, true, undefined, undefined, newVariant, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { shiny: true, variant: newVariant }); this.scene.playSound("se/sparkle"); // Set the variant label to the shiny tint @@ -1873,12 +1880,40 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonShinyIcon.setTint(tint); this.pokemonShinyIcon.setVisible(true); } else { - this.setSpeciesDetails(this.lastSpecies, false, undefined, undefined, 0, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { shiny: false, variant: 0 }); this.pokemonShinyIcon.setVisible(false); success = true; } } break; + case Button.V: + if (this.canCycleVariant) { + let newVariant = props.variant; + do { + newVariant = (newVariant + 1) % 3; + if (newVariant === 0) { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT) { // TODO: is this bang correct? + break; + } + } else if (newVariant === 1) { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2) { // TODO: is this bang correct? + break; + } + } else { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3) { // TODO: is this bang correct? + break; + } + } + } while (newVariant !== props.variant); + starterAttributes.variant = newVariant; // store the selected variant + this.setSpeciesDetails(this.lastSpecies, { variant: newVariant as Variant }); + // Cycle tint based on current sprite tint + const tint = getVariantTint(newVariant as Variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); + this.pokemonShinyIcon.setTint(tint); + success = true; + } + break; case Button.CYCLE_FORM: if (this.canCycleForm) { const formCount = this.lastSpecies.forms.length; @@ -1890,14 +1925,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } while (newFormIndex !== props.formIndex); starterAttributes.form = newFormIndex; // store the selected form - this.setSpeciesDetails(this.lastSpecies, undefined, newFormIndex, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex }); success = true; } break; case Button.CYCLE_GENDER: if (this.canCycleGender) { starterAttributes.female = !props.female; - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, !props.female, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies, { female: !props.female }); success = true; } break; @@ -1934,7 +1969,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.scene.ui.editTooltip(`${newAbility.name}`, `${newAbility.description}`); } - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, newAbilityIndex, undefined); + this.setSpeciesDetails(this.lastSpecies, { abilityIndex: newAbilityIndex }); success = true; } break; @@ -1945,35 +1980,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const newNature = natures[natureIndex < natures.length - 1 ? natureIndex + 1 : 0]; // store cycled nature as default starterAttributes.nature = newNature as unknown as integer; - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, newNature, undefined); - success = true; - } - break; - case Button.V: - if (this.canCycleVariant) { - let newVariant = props.variant; - do { - newVariant = (newVariant + 1) % 3; - if (!newVariant) { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT) { // TODO: is this bang correct? - break; - } - } else if (newVariant === 1) { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2) { // TODO: is this bang correct? - break; - } - } else { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3) { // TODO: is this bang correct? - break; - } - } - } while (newVariant !== props.variant); - starterAttributes.variant = newVariant; // store the selected variant - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, newVariant as Variant, undefined, undefined); - // Cycle tint based on current sprite tint - const tint = getVariantTint(newVariant as Variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); - this.pokemonShinyIcon.setTint(tint); + this.setSpeciesDetails(this.lastSpecies, { natureIndex: newNature }); success = true; } break; @@ -2190,7 +2197,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } else { this.scene.gameData.starterData[speciesId].moveset = this.starterMoveset?.slice(0) as StarterMoveset; } - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined, false); + this.setSpeciesDetails(this.lastSpecies, { forSeen: false }); // switch moves of starter if exists if (this.starterMovesets.length) { @@ -2320,8 +2327,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } - getValueLimit(): integer { - const valueLimit = new Utils.IntegerHolder(0); + getValueLimit(): number { + const valueLimit = new NumberHolder(0); switch (this.scene.gameMode.modeId) { case GameModes.ENDLESS: case GameModes.SPLICED_ENDLESS: @@ -2357,12 +2364,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { * Since some pokemon rely on forms to be valid (i.e. blaze tauros for fire challenges), we make a fake form and dex props to use in the challenge */ const tempFormProps = BigInt(Math.pow(2, i)) * DexAttr.DEFAULT_FORM; - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, container.species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, tempFormProps), true); allFormsValid = allFormsValid || isValidForChallenge.value; } } else { - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, container.species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, this.scene.gameData.getSpeciesDefaultDexAttr(container.species, false, true)), true); allFormsValid = isValidForChallenge.value; } @@ -2624,8 +2631,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } // Set the candy colors - container.candyUpgradeIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(starterColors[speciesId][0]))); - container.candyUpgradeOverlayIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(starterColors[speciesId][1]))); + container.candyUpgradeIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][0]))); + container.candyUpgradeOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][1]))); this.setUpgradeIcon(container); } else if (this.scene.candyUpgradeDisplay === 1) { @@ -2760,7 +2767,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.lastSpecies = species!; // TODO: is this bang correct? if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { - this.pokemonNumberText.setText(Utils.padInt(species.speciesId, 4)); + this.pokemonNumberText.setText(padInt(species.speciesId, 4)); if (starterAttributes?.nickname) { const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); this.pokemonNameText.setText(name); @@ -2778,7 +2785,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); //Growth translate - let growthReadable = Utils.toReadableString(GrowthRate[species.growthRate]); + let growthReadable = toReadableString(GrowthRate[species.growthRate]); const growthAux = growthReadable.replace(" ", "_"); if (i18next.exists("growth:" + growthAux)) { growthReadable = i18next.t("growth:" + growthAux as any); @@ -2826,9 +2833,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } else { this.pokemonCaughtHatchedContainer.setY(25); this.pokemonShinyIcon.setY(117); - this.pokemonCandyIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(colorScheme[0]))); + this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); this.pokemonCandyIcon.setVisible(true); - this.pokemonCandyOverlayIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(colorScheme[1]))); + this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); this.pokemonCandyOverlayIcon.setVisible(true); this.pokemonCandyDarknessOverlay.setVisible(true); this.pokemonCandyCountText.setText(`x${this.scene.gameData.starterData[species.speciesId].candyCount}`); @@ -2875,7 +2882,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (starterIndex > -1) { props = this.scene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]); - this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, this.starterAbilityIndexes[starterIndex], this.starterNatures[starterIndex]); + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex: this.starterAbilityIndexes[starterIndex], + natureIndex: this.starterNatures[starterIndex] + }); } else { const defaultDexAttr = this.getCurrentDexProps(species.speciesId); const defaultAbilityIndex = starterAttributes?.ability ?? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species); @@ -2890,7 +2904,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { props.formIndex = starterAttributes?.form ?? props.formIndex; props.female = starterAttributes?.female ?? props.female; - this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, defaultAbilityIndex, defaultNature); + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex: defaultAbilityIndex, + natureIndex: defaultNature + }); } const speciesForm = getPokemonSpeciesForm(species.speciesId, props.formIndex); @@ -2924,11 +2945,19 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const defaultNature = this.scene.gameData.getSpeciesDefaultNature(species); const props = this.scene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, defaultAbilityIndex, defaultNature, true); + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex: defaultAbilityIndex, + natureIndex: defaultNature, + forSeen: true + }); this.pokemonSprite.setTint(0x808080); } } else { - this.pokemonNumberText.setText(Utils.padInt(0, 4)); + this.pokemonNumberText.setText(padInt(0, 4)); this.pokemonNameText.setText(species ? "???" : ""); this.pokemonGrowthRateText.setText(""); this.pokemonGrowthRateLabelText.setVisible(false); @@ -2948,13 +2977,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCandyCountText.setVisible(false); this.pokemonFormText.setVisible(false); - this.setSpeciesDetails(species!, false, 0, false, 0, 0, 0); // TODO: is this bang correct? + this.setSpeciesDetails(species!, { // TODO: is this bang correct? + shiny: false, + formIndex: 0, + female: false, + variant: 0, + abilityIndex: 0, + natureIndex: 0 + }); this.pokemonSprite.clearTint(); } } - - setSpeciesDetails(species: PokemonSpecies, shiny?: boolean, formIndex?: integer, female?: boolean, variant?: Variant, abilityIndex?: integer, natureIndex?: integer, forSeen: boolean = false): void { + setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { + let { shiny, formIndex, female, variant, abilityIndex, natureIndex } = options; + const forSeen: boolean = options.forSeen ?? false; const oldProps = species ? this.scene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; const oldAbilityIndex = this.abilityCursor > -1 ? this.abilityCursor : this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species); const oldNatureIndex = this.natureCursor > -1 ? this.natureCursor : this.scene.gameData.getSpeciesDefaultNature(species); @@ -2962,6 +2999,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.abilityCursor = -1; this.natureCursor = -1; + // We will only update the sprite if there is a change to form, shiny/variant + // or gender for species with gender sprite differences + const shouldUpdateSprite = (species?.genderDiffs && !isNullOrUndefined(female)) + || !isNullOrUndefined(formIndex) || !isNullOrUndefined(shiny) || !isNullOrUndefined(variant); + if (this.activeTooltip === "CANDY") { if (this.lastSpecies) { const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId); @@ -3050,24 +3092,27 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterNatures[starterIndex] = this.natureCursor; } - const assetLoadCancelled = new Utils.BooleanHolder(false); + const assetLoadCancelled = new BooleanHolder(false); this.assetLoadCancelled = assetLoadCancelled; - species.loadAssets(this.scene, female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? - if (assetLoadCancelled.value) { - return; - } - this.assetLoadCancelled = null; - this.speciesLoaded.set(species.speciesId, true); - this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? - this.pokemonSprite.setPipelineData("shiny", shiny); - this.pokemonSprite.setPipelineData("variant", variant); - this.pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + if (shouldUpdateSprite) { + species.loadAssets(this.scene, female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? + if (assetLoadCancelled.value) { + return; + } + this.assetLoadCancelled = null; + this.speciesLoaded.set(species.speciesId, true); + this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + this.pokemonSprite.setPipelineData("shiny", shiny); + this.pokemonSprite.setPipelineData("variant", variant); + this.pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + this.pokemonSprite.setVisible(!this.statsMode); + }); + } else { this.pokemonSprite.setVisible(!this.statsMode); - }); + } - - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor), !!this.starterSpecies.length); const currentFilteredContainer = this.filteredStarterContainers.find(p => p.species.speciesId === species.speciesId); if (currentFilteredContainer) { @@ -3224,9 +3269,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }) as StarterMoveset; const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? - const formText = Utils.capitalizeString(species?.forms[formIndex!]?.formKey, "-", false, false); // TODO: is the bang correct? + const formText = capitalizeString(species?.forms[formIndex!]?.formKey, "-", false, false); // TODO: is the bang correct? - const speciesName = Utils.capitalizeString(Species[species.speciesId], "_", true, false); + const speciesName = capitalizeString(Species[species.speciesId], "_", true, false); if (species.speciesId === Species.ARCEUS) { this.pokemonFormText.setText(i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`)); @@ -3392,12 +3437,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.valueLimitLabel.setColor(this.getTextColor(!overLimit ? TextStyle.TOOLTIP_CONTENT : TextStyle.SUMMARY_PINK)); this.valueLimitLabel.setShadowColor(this.getTextColor(!overLimit ? TextStyle.TOOLTIP_CONTENT : TextStyle.SUMMARY_PINK, true)); if (overLimit) { - this.scene.time.delayedCall(Utils.fixedInt(500), () => this.tryUpdateValue()); + this.scene.time.delayedCall(fixedInt(500), () => this.tryUpdateValue()); return false; } let isPartyValid: boolean = this.isPartyValid(); // this checks to see if the party is valid if (addingToParty) { // this does a check to see if the pokemon being added is valid; if so, it will update the isPartyValid boolean - const isNewPokemonValid = new Utils.BooleanHolder(true); + const isNewPokemonValid = new BooleanHolder(true); const species = this.filteredStarterContainers[this.cursor].species; Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isNewPokemonValid, this.scene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), false); isPartyValid = isPartyValid || isNewPokemonValid.value; @@ -3406,13 +3451,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { /** * this loop is used to set the Sprite's alpha value and check if the user can select other pokemon more. */ - this.canAddParty = false; const remainValue = valueLimit - newValue; for (let s = 0; s < this.allSpecies.length; s++) { /** Cost of pokemon species */ const speciesStarterValue = this.scene.gameData.getSpeciesStarterValue(this.allSpecies[s].speciesId); - /** Used to detect if this pokemon is registered in starter */ - const speciesStarterDexEntry = this.scene.gameData.dexData[this.allSpecies[s].speciesId]; /** {@linkcode Phaser.GameObjects.Sprite} object of Pokémon for setting the alpha value */ const speciesSprite = this.starterContainers[s].icon; @@ -3427,7 +3469,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { * If speciesStarterDexEntry?.caughtAttr is true, this species registered in stater. * we change to can AddParty value to true since the user has enough cost to choose this pokemon and this pokemon registered too. */ - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, this.allSpecies[s], isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(this.allSpecies[s], this.getCurrentDexProps(this.allSpecies[s].speciesId)), isPartyValid); const canBeChosen = remainValue >= speciesStarterValue && isValidForChallenge.value; @@ -3443,9 +3485,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { */ if (canBeChosen || (isPokemonInParty && remainValue >= speciesStarterValue)) { speciesSprite.setAlpha(1); - if (speciesStarterDexEntry?.caughtAttr) { - this.canAddParty = true; - } } else { /** * If it can't be chosen, the user can't select. @@ -3455,7 +3494,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } - this.value = newValue; return true; } @@ -3543,7 +3581,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { isPartyValid(): boolean { let canStart = false; for (let s = 0; s < this.starterSpecies.length; s++) { - const isValidForChallenge = new Utils.BooleanHolder(true); + const isValidForChallenge = new BooleanHolder(true); const species = this.starterSpecies[s]; Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), false); canStart = canStart || isValidForChallenge.value; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index b35c0ca3303..9a61dd0f688 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -554,7 +554,7 @@ export default class SummaryUiHandler extends UiHandler { break; } const isDown = button === Button.DOWN; - const party = this.scene.getParty(); + const party = this.scene.getPlayerParty(); const partyMemberIndex = this.pokemon ? party.indexOf(this.pokemon) : -1; if ((isDown && partyMemberIndex < party.length - 1) || (!isDown && partyMemberIndex)) { const page = this.cursor; diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 4c55a4b960e..ecc15e5985e 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -184,6 +184,7 @@ export default class TargetSelectUiHandler extends UiHandler { } clear() { + this.cursor = -1; super.clear(); this.eraseCursor(); } diff --git a/src/utils.ts b/src/utils.ts index 8a35a4b3f07..b615dcf122b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -487,7 +487,7 @@ export function verifyLang(lang?: string): boolean { } switch (lang) { - case "es": + case "es-ES": case "fr": case "de": case "it":