diff --git a/CREDITS.md b/CREDITS.md index fd9a3d7bde3..099d410417e 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -343,34 +343,39 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. # 💻 Development -## Server Owner/Administrator +## Server Developers - pancakes aka patapancakes -## Senior Developers -- Walker -- NightKev -- Moka -- Temp aka Tempo-anon -- Madmadness65 - -## Developers +## Current and former Development Team members +- bennybroseph +- Brain Frog - CodeTappert +- Dakurei - flx-sta -- innerthunder - frutescens +- Greenlamp +- ImperialSympathizer +- innerthunder +- KimJeongSun +- Madmadness65 +- Moka +- Navori +- NightKev - Opaquer +- OrangeRed +- Sam aka Flashfyre (initial developer, started PokéRogue) +- sirzento - SN34KZ - Swain aka torranx - -## Junior Developers -- KimJeongSun -- ImperialSympathizer +- Temp aka Tempo-anon +- Walker +- Xavion ## Bug/Issue Managers -- Snailman - Daleks - Lily - PigeonBar +- Snailman ## Other Code Contributors - Admiral-Billy @@ -378,10 +383,7 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - arColm - Arxalc - AsdarDevelops -- bennybroseph -- Brain Frog - Corrade -- Dakurei - DustinLin - ElizaAlex - EmberCM @@ -391,7 +393,6 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - francktrouillez - FredeX - geeilhan -- Greenlamp - happinyz - hayuna - InfernoVulpix @@ -411,7 +412,6 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - Neverblade - NxKarim - okimin -- OrangeRed - PigeonBar - PrabbyDD - prateau @@ -421,10 +421,8 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - RedstonewolfX - ReneGV - rnicar245 -- Sam aka Flashfyre (initial developer, started PokéRogue) - schmidtc1 - shayebeadling -- sirzento - snoozbuster - sodaMelon - td76099 diff --git a/public/fonts/pokemon-emerald-pro.ttf b/public/fonts/pokemon-emerald-pro.ttf index 758130172c7..d63ff2d6470 100644 Binary files a/public/fonts/pokemon-emerald-pro.ttf and b/public/fonts/pokemon-emerald-pro.ttf differ diff --git a/src/data/ability.ts b/src/data/ability.ts index 4c9c44c6f35..c19b6fe9ba4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2643,55 +2643,6 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr { } } -/** - * Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon switching out. - */ -export class PreSwitchOutClearWeatherAbAttr extends PreSwitchOutAbAttr { - - /** - * @param pokemon The {@linkcode Pokemon} with the ability - * @param passive N/A - * @param args N/A - * @returns {boolean} Returns true if the weather clears, otherwise false. - */ - applyPreSwitchOut(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { - const weatherType = globalScene.arena.weather?.weatherType; - let turnOffWeather = false; - - // Clear weather only if user's ability matches the weather and no other pokemon has the ability. - switch (weatherType) { - case (WeatherType.HARSH_SUN): - if (pokemon.hasAbility(Abilities.DESOLATE_LAND) - && globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DESOLATE_LAND)).length === 0) { - turnOffWeather = true; - } - break; - case (WeatherType.HEAVY_RAIN): - if (pokemon.hasAbility(Abilities.PRIMORDIAL_SEA) - && globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.PRIMORDIAL_SEA)).length === 0) { - turnOffWeather = true; - } - break; - case (WeatherType.STRONG_WINDS): - if (pokemon.hasAbility(Abilities.DELTA_STREAM) - && globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DELTA_STREAM)).length === 0) { - turnOffWeather = true; - } - break; - } - - if (simulated) { - return turnOffWeather; - } - - if (turnOffWeather) { - globalScene.arena.trySetWeather(WeatherType.NONE, false); - return true; - } - - return false; - } -} export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr { applyPreSwitchOut(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { @@ -2744,6 +2695,61 @@ export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr { } +export class PreLeaveFieldAbAttr extends AbAttr { + applyPreLeaveField(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { + return false; + } +} + +/** + * Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon switching out. + */ +export class PreLeaveFieldClearWeatherAbAttr extends PreLeaveFieldAbAttr { + /** + * @param pokemon The {@linkcode Pokemon} with the ability + * @param passive N/A + * @param args N/A + * @returns Returns `true` if the weather clears, otherwise `false`. + */ + applyPreLeaveField(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { + const weatherType = globalScene.arena.weather?.weatherType; + let turnOffWeather = false; + + // Clear weather only if user's ability matches the weather and no other pokemon has the ability. + switch (weatherType) { + case (WeatherType.HARSH_SUN): + if (pokemon.hasAbility(Abilities.DESOLATE_LAND) + && globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DESOLATE_LAND)).length === 0) { + turnOffWeather = true; + } + break; + case (WeatherType.HEAVY_RAIN): + if (pokemon.hasAbility(Abilities.PRIMORDIAL_SEA) + && globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.PRIMORDIAL_SEA)).length === 0) { + turnOffWeather = true; + } + break; + case (WeatherType.STRONG_WINDS): + if (pokemon.hasAbility(Abilities.DELTA_STREAM) + && globalScene.getField(true).filter(p => p !== pokemon).filter(p => p.hasAbility(Abilities.DELTA_STREAM)).length === 0) { + turnOffWeather = true; + } + break; + } + + if (simulated) { + return turnOffWeather; + } + + if (turnOffWeather) { + globalScene.arena.trySetWeather(WeatherType.NONE, false); + return true; + } + + return false; + } +} + export class PreStatStageChangeAbAttr extends AbAttr { applyPreStatStageChange(pokemon: Pokemon | null, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { return false; @@ -4171,59 +4177,6 @@ export class PostFaintUnsuppressedWeatherFormChangeAbAttr extends PostFaintAbAtt } } -/** - * Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon fainting - */ -export class PostFaintClearWeatherAbAttr extends PostFaintAbAttr { - - /** - * @param pokemon The {@linkcode Pokemon} with the ability - * @param passive N/A - * @param attacker N/A - * @param move N/A - * @param hitResult N/A - * @param args N/A - * @returns {boolean} Returns true if the weather clears, otherwise false. - */ - applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): boolean { - const weatherType = globalScene.arena.weather?.weatherType; - let turnOffWeather = false; - - // Clear weather only if user's ability matches the weather and no other pokemon has the ability. - switch (weatherType) { - case (WeatherType.HARSH_SUN): - if (pokemon.hasAbility(Abilities.DESOLATE_LAND) - && globalScene.getField(true).filter(p => p.hasAbility(Abilities.DESOLATE_LAND)).length === 0) { - turnOffWeather = true; - } - break; - case (WeatherType.HEAVY_RAIN): - if (pokemon.hasAbility(Abilities.PRIMORDIAL_SEA) - && globalScene.getField(true).filter(p => p.hasAbility(Abilities.PRIMORDIAL_SEA)).length === 0) { - turnOffWeather = true; - } - break; - case (WeatherType.STRONG_WINDS): - if (pokemon.hasAbility(Abilities.DELTA_STREAM) - && globalScene.getField(true).filter(p => p.hasAbility(Abilities.DELTA_STREAM)).length === 0) { - turnOffWeather = true; - } - break; - } - - if (simulated) { - return turnOffWeather; - } - - if (turnOffWeather) { - globalScene.arena.trySetWeather(WeatherType.NONE, false); - return true; - } - - return false; - } -} - export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { private damageRatio: number; @@ -5229,6 +5182,11 @@ export function applyPreSwitchOutAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), args, true, simulated); } +export function applyPreLeaveFieldAbAttrs(attrType: Constructor, + pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, args), args, true, simulated); +} + export function applyPreStatStageChangeAbAttrs(attrType: Constructor, pokemon: Pokemon | null, stat: BattleStat, cancelled: Utils.BooleanHolder, simulated: boolean = false, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), args, false, simulated); @@ -5912,20 +5870,17 @@ export function initAbilities() { new Ability(Abilities.PRIMORDIAL_SEA, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.HEAVY_RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HEAVY_RAIN) - .attr(PreSwitchOutClearWeatherAbAttr) - .attr(PostFaintClearWeatherAbAttr) + .attr(PreLeaveFieldClearWeatherAbAttr) .bypassFaint(), new Ability(Abilities.DESOLATE_LAND, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.HARSH_SUN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HARSH_SUN) - .attr(PreSwitchOutClearWeatherAbAttr) - .attr(PostFaintClearWeatherAbAttr) + .attr(PreLeaveFieldClearWeatherAbAttr) .bypassFaint(), new Ability(Abilities.DELTA_STREAM, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.STRONG_WINDS) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.STRONG_WINDS) - .attr(PreSwitchOutClearWeatherAbAttr) - .attr(PostFaintClearWeatherAbAttr) + .attr(PreLeaveFieldClearWeatherAbAttr) .bypassFaint(), new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 17dc34aa514..c399a9bb595 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2871,7 +2871,7 @@ export class SyrupBombTag extends BattlerTag { /** * Telekinesis raises the target into the air for three turns and causes all moves used against the target (aside from OHKO moves) to hit the target unless the target is in a semi-invulnerable state from Fly/Dig. * The first effect is provided by {@linkcode FloatingTag}, the accuracy-bypass effect is provided by TelekinesisTag - * The effects of Telekinesis can be baton passed to a teammate. Unlike the mainline games, Telekinesis can be baton-passed to Mega Gengar. + * The effects of Telekinesis can be baton passed to a teammate. * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS} */ export class TelekinesisTag extends BattlerTag { diff --git a/src/data/move.ts b/src/data/move.ts index 5bc37b4e8e8..48f90297115 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -10429,9 +10429,8 @@ 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) // Custom - .triageMove() - .target(MoveTarget.ALL_NEAR_ENEMIES), + .attr(HitHealAttr, 1) + .triageMove(), new AttackMove(Moves.BUZZY_BUZZ, Type.ELECTRIC, MoveCategory.SPECIAL, 60, 100, 20, 100, 0, 7) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.SIZZLY_SLIDE, Type.FIRE, MoveCategory.PHYSICAL, 60, 100, 20, 100, 0, 7) diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 6dcac277525..88e5794e816 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -148,7 +148,7 @@ export const DancingLessonsEncounter: MysteryEncounter = // Adds a real Pokemon sprite to the field (required for the animation) globalScene.getEnemyParty().forEach(enemyPokemon => { - globalScene.field.remove(enemyPokemon, true); + enemyPokemon.leaveField(true, true, true); }); globalScene.currentBattle.enemyParty = [ oricorio ]; globalScene.field.add(oricorio); 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 4556c3ab6a0..53f89069491 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -229,7 +229,7 @@ function handleLoseMinigame() { // End the battle if (wobbuffet) { wobbuffet.hideInfo(); - globalScene.field.remove(wobbuffet); + wobbuffet.leaveField(); } transitionMysteryEncounterIntroVisuals(true, true); globalScene.currentBattle.enemyParty = []; @@ -278,7 +278,7 @@ function handleNextTurn() { // End the battle wobbuffet.hideInfo(); - globalScene.field.remove(wobbuffet); + wobbuffet.leaveField(); globalScene.currentBattle.enemyParty = []; globalScene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; leaveEncounterWithoutBattle(isHealPhase); diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index 84768519bea..16015c80fc8 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -168,6 +168,7 @@ async function doBiomeTransitionDialogueAndBattleInit() { // Show dialogue and transition biome await showEncounterText(`${namespace}:transport`); await Promise.all([ animateBiomeChange(newBiome), transitionMysteryEncounterIntroVisuals() ]); + globalScene.updateBiomeWaveText(); globalScene.playBgm(); await showEncounterText(`${namespace}:attacked`); 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 5e87a40d952..2f5843e39d2 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 @@ -575,7 +575,7 @@ function onGameOver() { ease: "Sine.easeIn", scale: 0.5, onComplete: () => { - globalScene.field.remove(pokemon, true); + pokemon.leaveField(true, true, true); } }); } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 8e7c67fae84..ead0443908b 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -164,7 +164,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): } globalScene.getEnemyParty().forEach(enemyPokemon => { - globalScene.field.remove(enemyPokemon, true); + enemyPokemon.leaveField(true, true, true); }); battle.enemyParty = []; battle.double = doubleBattle; @@ -810,7 +810,7 @@ export function transitionMysteryEncounterIntroVisuals(hide: boolean = true, des globalScene.field.remove(introVisuals, true); enemyPokemon.forEach(pokemon => { - globalScene.field.remove(pokemon, true); + pokemon.leaveField(true, true, true); }); globalScene.currentBattle.mysteryEncounter!.introVisuals = undefined; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 2d0081f19cd..580aaaf2cc6 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -592,7 +592,7 @@ export async function catchPokemon(pokemon: EnemyPokemon, pokeball: Phaser.GameO }; const removePokemon = () => { if (pokemon) { - globalScene.field.remove(pokemon, true); + pokemon.leaveField(false, true, true); } }; const addToParty = (slotIndex?: number) => { @@ -695,7 +695,7 @@ export async function doPokemonFlee(pokemon: EnemyPokemon): Promise { scale: pokemon.getSpriteScale(), onComplete: () => { pokemon.setVisible(false); - globalScene.field.remove(pokemon, true); + pokemon.leaveField(true, true, true); showEncounterText(i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) .then(() => { resolve(); @@ -723,7 +723,7 @@ export function doPlayerFlee(pokemon: EnemyPokemon): Promise { scale: pokemon.getSpriteScale(), onComplete: () => { pokemon.setVisible(false); - globalScene.field.remove(pokemon, true); + pokemon.leaveField(true, true, true); showEncounterText(i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) .then(() => { resolve(); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ebf665ce5e6..16af8364502 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -31,7 +31,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability"; +import { 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, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1422,8 +1422,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ public hasPassive(): boolean { // returns override if valid for current case - if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) - || (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) { + if ( + (Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) + || (Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && !this.isPlayer()) + ) { + return false; + } + if ( + ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) && this.isPlayer()) + || ((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) && !this.isPlayer()) + ) { return true; } @@ -3226,7 +3234,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } for (const tag of source.summonData.tags) { - if (!tag.isBatonPassable) { + if (!tag.isBatonPassable || (tag.tagType === BattlerTagType.TELEKINESIS && this.species.speciesId === Species.GENGAR && this.getFormKey() === "mega")) { continue; } @@ -4142,9 +4150,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param hideInfo Indicates if this should also play the animation to hide the Pokemon's * info container. */ - leaveField(clearEffects: boolean = true, hideInfo: boolean = true) { + leaveField(clearEffects: boolean = true, hideInfo: boolean = true, destroy: boolean = false) { this.resetSprite(); this.resetTurnData(); + globalScene.getField(true).filter(p => p !== this).forEach(p => p.removeTagsBySourceId(this.id)); + if (clearEffects) { this.destroySubstitute(); this.resetSummonData(); // this also calls `resetBattleSummonData` @@ -4152,9 +4162,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (hideInfo) { this.hideInfo(); } - globalScene.field.remove(this); + // Trigger abilities that activate upon leaving the field + applyPreLeaveFieldAbAttrs(PreLeaveFieldAbAttr, this); this.setSwitchOutStatus(true); globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); + globalScene.field.remove(this, destroy); } destroy(): void { diff --git a/src/overrides.ts b/src/overrides.ts index 8f881ca59dd..e53d3b766c4 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -129,6 +129,7 @@ class DefaultOverrides { readonly STARTER_FUSION_SPECIES_OVERRIDE: Species | number = 0; readonly ABILITY_OVERRIDE: Abilities = Abilities.NONE; readonly PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; + readonly HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null; readonly STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; readonly GENDER_OVERRIDE: Gender | null = null; readonly MOVESET_OVERRIDE: Moves | Array = []; @@ -150,6 +151,7 @@ class DefaultOverrides { readonly OPP_LEVEL_OVERRIDE: number = 0; readonly OPP_ABILITY_OVERRIDE: Abilities = Abilities.NONE; readonly OPP_PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; + readonly OPP_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null; readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; readonly OPP_GENDER_OVERRIDE: Gender | null = null; readonly OPP_MOVESET_OVERRIDE: Moves | Array = []; diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 6a6a2efa061..77a8043aee9 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -241,11 +241,10 @@ export class AttemptCapturePhase extends PokemonPhase { }; const removePokemon = () => { globalScene.addFaintedEnemyScore(pokemon); - globalScene.getPlayerField().filter(p => p.isActive(true)).forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id)); pokemon.hp = 0; pokemon.trySetStatus(StatusEffect.FAINT); globalScene.clearEnemyHeldItemModifiers(); - globalScene.field.remove(pokemon, true); + pokemon.leaveField(true, true, true); }; const addToParty = (slotIndex?: number) => { const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 7bf3bc81930..4a0c7ead123 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -181,9 +181,7 @@ export class FaintPhase extends PokemonPhase { y: pokemon.y + 150, ease: "Sine.easeIn", onComplete: () => { - pokemon.resetSprite(); pokemon.lapseTags(BattlerTagLapseType.FAINT); - globalScene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); pokemon.y -= 150; pokemon.trySetStatus(StatusEffect.FAINT); @@ -193,7 +191,7 @@ export class FaintPhase extends PokemonPhase { globalScene.addFaintedEnemyScore(pokemon as EnemyPokemon); globalScene.currentBattle.addPostBattleLoot(pokemon as EnemyPokemon); } - globalScene.field.remove(pokemon); + pokemon.leaveField(); this.end(); } }); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index d24ef68ebb2..dad0f6f11ad 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -64,6 +64,7 @@ export class SwitchSummonPhase extends SummonPhase { const pokemon = this.getPokemon(); (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); + if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { const substitute = pokemon.getTag(SubstituteTag); if (substitute) { @@ -93,8 +94,8 @@ export class SwitchSummonPhase extends SummonPhase { ease: "Sine.easeIn", scale: 0.5, onComplete: () => { - pokemon.leaveField(this.switchType === SwitchType.SWITCH, false); globalScene.time.delayedCall(750, () => this.switchAndSummon()); + pokemon.leaveField(this.switchType === SwitchType.SWITCH, false); } }); } diff --git a/src/test/abilities/desolate-land.test.ts b/src/test/abilities/desolate-land.test.ts new file mode 100644 index 00000000000..75576d7a8f6 --- /dev/null +++ b/src/test/abilities/desolate-land.test.ts @@ -0,0 +1,139 @@ +import { PokeballType } from "#app/enums/pokeball"; +import { WeatherType } from "#app/enums/weather-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, it, expect, vi } from "vitest"; + +describe("Abilities - Desolate Land", () => { + 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) + .hasPassiveAbility(true) + .enemySpecies(Species.RALTS) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + /** + * This checks that the weather has changed after the Enemy Pokemon with {@linkcode Abilities.DESOLATE_LAND} + * is forcefully moved out of the field from moves such as Roar {@linkcode Moves.ROAR} + */ + it("should lift only when all pokemon with this ability leave the field", async () => { + game.override + .battleType("double") + .enemyMoveset([ Moves.SPLASH, Moves.ROAR ]); + await game.classicMode.startBattle([ Species.MAGCARGO, Species.MAGCARGO, Species.MAGIKARP, Species.MAGIKARP ]); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => { + return min; + }); + + game.move.select(Moves.SPLASH, 0, 2); + game.move.select(Moves.SPLASH, 1, 2); + + await game.forceEnemyMove(Moves.ROAR, 0); + await game.forceEnemyMove(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + await game.toNextTurn(); + + vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => { + return min + 1; + }); + + game.move.select(Moves.SPLASH, 0, 2); + game.move.select(Moves.SPLASH, 1, 2); + + await game.forceEnemyMove(Moves.ROAR, 1); + await game.forceEnemyMove(Moves.SPLASH, 0); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN); + }); + + it("should lift when enemy faints", async () => { + game.override + .battleType("single") + .moveset([ Moves.SHEER_COLD ]) + .ability(Abilities.NO_GUARD) + .startingLevel(100) + .enemyLevel(1) + .enemyMoveset([ Moves.SPLASH ]) + .enemySpecies(Species.MAGCARGO) + .enemyHasPassiveAbility(true); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + game.move.select(Moves.SHEER_COLD); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN); + }); + + it("should lift when pokemon returns upon switching from double to single battle", async () => { + game.override + .battleType("even-doubles") + .enemyMoveset([ Moves.SPLASH, Moves.MEMENTO ]) + .startingWave(12); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGCARGO ]); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + game.move.select(Moves.SPLASH, 0, 2); + game.move.select(Moves.SPLASH, 1, 2); + await game.forceEnemyMove(Moves.MEMENTO, 0); + await game.forceEnemyMove(Moves.MEMENTO, 1); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + await game.toNextWave(); + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN); + }); + + it("should lift when enemy is captured", async () => { + game.override + .battleType("single") + .enemyMoveset([ Moves.SPLASH ]) + .enemySpecies(Species.MAGCARGO) + .enemyHasPassiveAbility(true); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + game.scene.pokeballCounts[PokeballType.MASTER_BALL] = 1; + + game.doThrowPokeball(PokeballType.MASTER_BALL); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN); + }); +}); diff --git a/src/test/moves/instruct.test.ts b/src/test/moves/instruct.test.ts index 13f0c83c784..b26f9c9669f 100644 --- a/src/test/moves/instruct.test.ts +++ b/src/test/moves/instruct.test.ts @@ -221,7 +221,8 @@ describe("Moves - Instruct", () => { it("should allow for dancer copying of instructed dance move", async () => { game.override .battleType("double") - .enemyMoveset([ Moves.INSTRUCT, Moves.SPLASH ]); + .enemyMoveset([ Moves.INSTRUCT, Moves.SPLASH ]) + .enemyLevel(1000); await game.classicMode.startBattle([ Species.ORICORIO, Species.VOLCARONA ]); const [ oricorio, volcarona ] = game.scene.getPlayerField(); @@ -236,11 +237,9 @@ describe("Moves - Instruct", () => { await game.phaseInterceptor.to("BerryPhase"); // fiery dance triggered dancer successfully for a total of 4 hits - // Volcarona fiery dance has a _small_ chance to 3HKO a shuckle in worst case, so we add the hit count of both - // foes to account for spillover + // Enemy level is set to a high value so that it does not faint even after all 4 hits instructSuccess(volcarona, Moves.FIERY_DANCE); - expect(game.scene.getEnemyField()[0].turnData.attacksReceived.length + - game.scene.getEnemyField()[1].turnData.attacksReceived.length).toBe(4); + expect(game.scene.getEnemyField()[0].turnData.attacksReceived.length).toBe(4); }); it("should not repeat move when switching out", async () => { diff --git a/src/test/moves/telekinesis.test.ts b/src/test/moves/telekinesis.test.ts index 76c0d001f00..ba2bc40a189 100644 --- a/src/test/moves/telekinesis.test.ts +++ b/src/test/moves/telekinesis.test.ts @@ -7,6 +7,7 @@ import { MoveResult } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; +import { BattlerIndex } from "#app/battle"; describe("Moves - Telekinesis", () => { let phaserGame: Phaser.Game; @@ -121,4 +122,17 @@ describe("Moves - Telekinesis", () => { expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined(); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); }); + + it("should not be baton passed onto a mega gengar", async () => { + game.override.moveset([ Moves.BATON_PASS ]) + .enemyMoveset([ Moves.TELEKINESIS ]) + .starterForms({ [Species.GENGAR]: 1 }); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.GENGAR ]); + game.move.select(Moves.BATON_PASS); + game.doSelectPartyPokemon(1); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined(); + }); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index fa7624c976e..b2015700c9b 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -24,6 +24,7 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; import ErrorInterceptor from "#app/test/utils/errorInterceptor"; import type InputsHandler from "#app/test/utils/inputsHandler"; +import type BallUiHandler from "#app/ui/ball-ui-handler"; import type BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; import type CommandUiHandler from "#app/ui/command-ui-handler"; import type ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; @@ -458,6 +459,24 @@ export default class GameManager { }); } + /** + * Select the BALL option from the command menu, then press Action; in the BALL + * menu, select a pokéball type and press Action again to throw it. + * @param ballIndex the index of the pokeball to throw + */ + public doThrowPokeball(ballIndex: number) { + this.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + (this.scene.ui.getHandler() as CommandUiHandler).setCursor(1); + (this.scene.ui.getHandler() as CommandUiHandler).processInput(Button.ACTION); + }); + + this.onNextPrompt("CommandPhase", Mode.BALL, () => { + const ballHandler = this.scene.ui.getHandler() as BallUiHandler; + ballHandler.setCursor(ballIndex); + ballHandler.processInput(Button.ACTION); // select ball and throw + }); + } + /** * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. * Used to manually modify Pokemon turn order. diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 822c42163b1..47358738048 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -181,6 +181,20 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Forces the status of the player (pokemon) **passive** {@linkcode Abilities | ability} + * @param hasPassiveAbility forces the passive to be active if `true`, inactive if `false` + * @returns `this` + */ + public hasPassiveAbility(hasPassiveAbility: boolean | null): this { + vi.spyOn(Overrides, "HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility); + if (hasPassiveAbility === null) { + this.log("Player Pokemon PASSIVE ability no longer force enabled or disabled!"); + } else { + this.log(`Player Pokemon PASSIVE ability is force ${hasPassiveAbility ? "enabled" : "disabled"}!`); + } + return this; + } /** * Override the player (pokemon) {@linkcode Moves | moves}set * @param moveset the {@linkcode Moves | moves}set to set @@ -325,6 +339,21 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Forces the status of the enemy (pokemon) **passive** {@linkcode Abilities | ability} + * @param hasPassiveAbility forces the passive to be active if `true`, inactive if `false` + * @returns `this` + */ + public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this { + vi.spyOn(Overrides, "OPP_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility); + if (hasPassiveAbility === null) { + this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!"); + } else { + this.log(`Enemy Pokemon PASSIVE ability is force ${hasPassiveAbility ? "enabled" : "disabled"}!`); + } + return this; + } + /** * Override the enemy (pokemon) {@linkcode Moves | moves}set * @param moveset the {@linkcode Moves | moves}set to set