From f89e42fa7beca8c424f0c451ab342e2fcd72f6ff Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:13:42 -0800 Subject: [PATCH 1/6] [Docs] Update `CREDITS.md` (#5257) * [Docs] Update `CREDITS.md` Add Xavion, condense dev team categories * Move Dakurei and OrangeRed to dev team list * Move sirzento to dev team list * Add Navori, move Sam --- CREDITS.md | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) 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 From 60990deaf2cf7f7d6c875cc09b7bdca85b667d04 Mon Sep 17 00:00:00 2001 From: Chris <75648912+ChrisLolz@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:14:28 -0500 Subject: [PATCH 2/6] [Bug] Update Biome text after using Teleporting Hijinks (#5173) --- .../encounters/teleporting-hijinks-encounter.ts | 1 + 1 file changed, 1 insertion(+) 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`); From 6c4dedb73e4cbc516cbbd16bab9149c3760cbc50 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:37:50 +0100 Subject: [PATCH 3/6] [Refactor/Bug] `Pokemon.leaveField()`, Fix Related Abilities (#5191) * Added new AbAttr that triggers whenever a pokemon leaves the field * Use leaveField everywhere * Changing order for PreSwitchOutAbAttr * Don't clearEffects when catching in a mystery encounter * Attempts to make new overrides for testing * New options in overrides * Implemented tests for Desolate Land * Fixing instruct test to not read turnData of fainted mon * Removed post faint clear weather * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Has_passive_ability override now turns off passives if set to "false", defaults to "null" * Updating overrides type definitions * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Suggestions from review * Fixed strings in suggestions * Simplified function to throw balls in tests * Added tsdocs to overrideHelper.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 171 +++++++----------- .../encounters/dancing-lessons-encounter.ts | 2 +- .../encounters/fun-and-games-encounter.ts | 4 +- .../the-expert-pokemon-breeder-encounter.ts | 2 +- .../utils/encounter-phase-utils.ts | 4 +- .../utils/encounter-pokemon-utils.ts | 6 +- src/field/pokemon.ts | 22 ++- src/overrides.ts | 2 + src/phases/attempt-capture-phase.ts | 3 +- src/phases/faint-phase.ts | 4 +- src/phases/switch-summon-phase.ts | 3 +- src/test/abilities/desolate-land.test.ts | 139 ++++++++++++++ src/test/moves/instruct.test.ts | 9 +- src/test/utils/gameManager.ts | 19 ++ src/test/utils/helpers/overridesHelper.ts | 29 +++ 15 files changed, 286 insertions(+), 133 deletions(-) create mode 100644 src/test/abilities/desolate-land.test.ts 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/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/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..8caa78aa7c9 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; } @@ -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/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 From c80489460cc9f0b5d00197aa1b7a83b80bbc0eae Mon Sep 17 00:00:00 2001 From: damocleas Date: Fri, 7 Feb 2025 15:28:25 -0500 Subject: [PATCH 4/6] [Bug] [Balance] Fix Bouncy Bubble being a spread move, revert back to 100% drain effect (#5269) --- src/data/move.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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) From e06a9df4cd912e4b4e9efafe9daf5b9532570dc5 Mon Sep 17 00:00:00 2001 From: Lugiad <2070109+Adri1@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:40:58 +0100 Subject: [PATCH 5/6] [UX/UI] Update emerald font for zero (again) (#5268) --- public/fonts/pokemon-emerald-pro.ttf | Bin 93788 -> 95224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/fonts/pokemon-emerald-pro.ttf b/public/fonts/pokemon-emerald-pro.ttf index 758130172c76eb3ddd06ced11258f00e8b92671e..d63ff2d6470d640eff3c77d034b0af86643721c2 100644 GIT binary patch delta 5096 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 OH}n=5<0}=`_x}rIOEE+M From 986fbf3cf74b95d55d7012a7a25a896f1cfcd424 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:00:25 -0600 Subject: [PATCH 6/6] [Move] [Bug] Fix super-niche edgecase with mega gengar and telekinesis (#5266) * Fix super-niche edgecase with mega gengar and telekinesis * Update TelekinesisTag doc comment * Remove comment about mega gengar as this update fixes it --- src/data/battler-tags.ts | 2 +- src/field/pokemon.ts | 2 +- src/test/moves/telekinesis.test.ts | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) 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/field/pokemon.ts b/src/field/pokemon.ts index 8caa78aa7c9..16af8364502 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3234,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; } 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(); + }); });