diff --git a/public/images/heavy_fog.png b/public/images/heavy_fog.png new file mode 100644 index 00000000000..6294e87dd52 Binary files /dev/null and b/public/images/heavy_fog.png differ diff --git a/public/images/items.json b/public/images/items.json index 4312f2a58c4..03557fbe315 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -8450,6 +8450,27 @@ "w": 16, "h": 16 } + }, + { + "filename": "micle_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 20, + "h": 20 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 20, + "h": 20 + }, + "frame": { + "x": 400, + "y": 386, + "w": 20, + "h": 20 + } } ] } diff --git a/public/images/items.png b/public/images/items.png index eb9878a5bfc..59edea42d0a 100644 Binary files a/public/images/items.png and b/public/images/items.png differ diff --git a/public/images/items/micle-berry.png b/public/images/items/micle-berry.png new file mode 100644 index 00000000000..41ca4f6628b Binary files /dev/null and b/public/images/items/micle-berry.png differ diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 120d1d413c4..7030e79285d 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -8118,7 +8118,7 @@ export function initAbilities() { .unreplaceable() .attr(NoFusionAbilityAbAttr) .attr(PostSummonFormChangeByWeatherAbAttr, AbilityId.FORECAST) - .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]), + .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HEAVY_FOG ]), new Ability(AbilityId.STICKY_HOLD, 3) .attr(BlockItemTheftAbAttr) .bypassFaint() @@ -8306,7 +8306,7 @@ export function initAbilities() { .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.SPDEF, 1.5) .attr(NoFusionAbilityAbAttr) .attr(PostSummonFormChangeByWeatherAbAttr, AbilityId.FLOWER_GIFT) - .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]) + .attr(PostWeatherChangeFormChangeAbAttr, AbilityId.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HEAVY_FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]) .uncopiable() .unreplaceable() .ignorable(), @@ -9018,7 +9018,7 @@ export function initAbilities() { .unreplaceable() .ignorable(), new Ability(AbilityId.TERAFORM_ZERO, 9) - .attr(ClearWeatherAbAttr, [ WeatherType.SUNNY, WeatherType.RAIN, WeatherType.SANDSTORM, WeatherType.HAIL, WeatherType.SNOW, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HARSH_SUN, WeatherType.STRONG_WINDS ]) + .attr(ClearWeatherAbAttr, [ WeatherType.SUNNY, WeatherType.RAIN, WeatherType.SANDSTORM, WeatherType.HAIL, WeatherType.SNOW, WeatherType.FOG, WeatherType.HEAVY_FOG, WeatherType.HEAVY_RAIN, WeatherType.HARSH_SUN, WeatherType.STRONG_WINDS ]) .attr(ClearTerrainAbAttr, [ TerrainType.MISTY, TerrainType.ELECTRIC, TerrainType.GRASSY, TerrainType.PSYCHIC ]) .uncopiable() .unreplaceable() diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 5dda1912e44..1fe9618272a 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1111,7 +1111,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.SLIGGOO, 40, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}) ], [SpeciesId.SLIGGOO]: [ - new SpeciesEvolution(SpeciesId.GOODRA, 50, null, {key: EvoCondKey.WEATHER, weather: [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]}, SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.GOODRA, 50, null, {key: EvoCondKey.WEATHER, weather: [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HEAVY_FOG ]}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.BERGMITE]: [ new SpeciesEvolution(SpeciesId.HISUI_AVALUGG, 37, null, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}), @@ -1334,7 +1334,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.HISUI_ZOROARK, 30, null, null) ], [SpeciesId.HISUI_SLIGGOO]: [ - new SpeciesEvolution(SpeciesId.HISUI_GOODRA, 50, null, {key: EvoCondKey.WEATHER, weather: [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]}, SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.HISUI_GOODRA, 50, null, {key: EvoCondKey.WEATHER, weather: [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HEAVY_FOG ]}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.SPRIGATITO]: [ new SpeciesEvolution(SpeciesId.FLORAGATO, 16, null, null) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f94c59bb463..dbeae843272 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -764,9 +764,6 @@ export default abstract class Move implements Localizable { applyMoveAttrs("VariableAccuracyAttr", user, target, this, moveAccuracy); applyPreDefendAbAttrs("WonderSkinAbAttr", target, user, this, { value: false }, simulated, moveAccuracy); - if (moveAccuracy.value === -1) { - return moveAccuracy.value; - } const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); @@ -774,7 +771,11 @@ export default abstract class Move implements Localizable { globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); } - if (globalScene.arena.weather?.weatherType === WeatherType.FOG) { + if (moveAccuracy.value === -1) { //Check accuracy after applying items modifier in case of Micle Berry + return moveAccuracy.value; + } + + if (globalScene.arena.weather?.weatherType === WeatherType.FOG || globalScene.arena.weather?.weatherType === WeatherType.HEAVY_FOG) { /** * The 0.9 multiplier is PokeRogue-only implementation, Bulbapedia uses 3/5 * See Fog {@link https://bulbapedia.bulbagarden.net/wiki/Fog} @@ -9401,7 +9402,7 @@ export function initMoves() { if (!weather) { return 1; } - const weatherTypes = [ WeatherType.SUNNY, WeatherType.RAIN, WeatherType.SANDSTORM, WeatherType.HAIL, WeatherType.SNOW, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HARSH_SUN ]; + const weatherTypes = [ WeatherType.SUNNY, WeatherType.RAIN, WeatherType.SANDSTORM, WeatherType.HAIL, WeatherType.SNOW, WeatherType.FOG, WeatherType.HEAVY_RAIN, WeatherType.HARSH_SUN, WeatherType.HEAVY_FOG ]; if (weatherTypes.includes(weather.weatherType) && !weather.isEffectSuppressed()) { return 2; } diff --git a/src/data/mystery-encounters/encounters/creeping-fog-encounter.ts b/src/data/mystery-encounters/encounters/creeping-fog-encounter.ts new file mode 100644 index 00000000000..ac83346e6a1 --- /dev/null +++ b/src/data/mystery-encounters/encounters/creeping-fog-encounter.ts @@ -0,0 +1,449 @@ +import type { EnemyPartyConfig, EnemyPokemonConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { + initBattleWithEnemyConfig, + setEncounterRewards, + leaveEncounterWithoutBattle, + transitionMysteryEncounterIntroVisuals, + generateModifierType, + setEncounterExp, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { BerryType } from "#enums/berry-type"; +import { randSeedInt } from "#app/utils/common"; +import { globalScene } from "#app/global-scene"; +import { modifierTypes } from "#app/data/data-lists"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { Nature } from "#enums/nature"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { TimeOfDayRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { TimeOfDay } from "#enums/time-of-day"; +import type { AbilityId } from "#enums/ability-id"; +import { Stat } from "#enums/stat"; +import type HeldModifierConfig from "#app/@types/held-modifier-config"; +import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { CustomPokemonData } from "#app/data/custom-pokemon-data"; +import { ModifierTier } from "#enums/modifier-tier"; +import { + MoveRequirement, + AbilityRequirement, + CombinationPokemonRequirement, +} from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { + DEFOG_MOVES, + DEFOG_ABILITIES, + LIGHT_ABILITIES, + LIGHT_MOVES, +} from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { BiomeId } from "#enums/biome-id"; +import { WeatherType } from "#enums/weather-type"; +import FogOverlay from "#app/ui/fog-overlay"; + +// the i18n namespace for the encounter +const namespace = "mysteryEncounters/creepingFog"; + +/** + * Creeping Fog Mystery Encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/4418 | GitHub Issue #4418} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + **/ +export const CreepingFogEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.CREEPING_FOG, +) + .withSceneRequirement(new TimeOfDayRequirement([TimeOfDay.DUSK, TimeOfDay.DAWN, TimeOfDay.NIGHT])) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(51, 179) + .withFleeAllowed(false) + .withIntroSpriteConfigs([]) + .withIntroDialogue([ + { + text: `${namespace}:intro`, + }, + ]) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) + .withOnInit(() => { + const waveIndex = globalScene.currentBattle.waveIndex; + const encounter = globalScene.currentBattle.mysteryEncounter!; + const chosenPokemonAttributes = chooseBoss(); + const chosenPokemon = chosenPokemonAttributes[0] as SpeciesId; + const naturePokemon = chosenPokemonAttributes[1] as Nature; + const abilityPokemon = chosenPokemonAttributes[2] as AbilityId; + const passivePokemon = chosenPokemon[3] as boolean; + const movesPokemon = chosenPokemonAttributes[4] as MoveId[]; + const modifPokemon = chosenPokemonAttributes[5] as HeldModifierConfig[]; + const segments = waveIndex < 80 ? 2 : waveIndex < 140 ? 3 : 4; + + const pokemonConfig: EnemyPokemonConfig = { + species: getPokemonSpecies(chosenPokemon), + formIndex: [SpeciesId.LYCANROC, SpeciesId.PIDGEOT].includes(chosenPokemon) ? 1 : 0, + isBoss: true, + shiny: false, + customPokemonData: new CustomPokemonData({ spriteScale: 1 + segments * 0.05 }), + nature: naturePokemon, + moveSet: movesPokemon, + abilityIndex: abilityPokemon, + passive: passivePokemon, + bossSegments: segments, + modifierConfigs: modifPokemon, + }; + + const config: EnemyPartyConfig = { + levelAdditiveModifier: 0.5, + pokemonConfigs: [pokemonConfig], + }; + encounter.enemyPartyConfigs = [config]; + encounter.spriteConfigs = [ + { + spriteKey: chosenPokemon.toString(), + fileRoot: "pokemon", + repeat: true, + hasShadow: true, + hidden: true, + x: 0, + tint: 1, + y: 0, + yShadow: -3, + }, + ]; + + const overlayWidth = globalScene.game.canvas.width / 6; + const overlayHeight = globalScene.game.canvas.height / 6 - 48; + const fogOverlay = new FogOverlay({ + delayVisibility: false, + scale: 1, + onSide: true, + right: true, + x: 1, + y: overlayHeight * -1 - 48, + width: overlayWidth, + height: overlayHeight, + }); + encounter.misc = { + fogOverlay, + }; + globalScene.ui.add(fogOverlay); + globalScene.ui.sendToBack(fogOverlay); + globalScene.tweens.add({ + targets: fogOverlay, + alpha: 0.5, + ease: "Sine.easeIn", + duration: 2000, + }); + fogOverlay.active = true; + fogOverlay.setVisible(true); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}:option.1.label`, + buttonTooltip: `${namespace}:option.1.tooltip`, + selected: [ + { + text: `${namespace}:option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async () => { + const encounter = globalScene.currentBattle.mysteryEncounter!; + globalScene.tweens.add({ + targets: encounter.misc.fogOverlay, + alpha: 0, + ease: "Sine.easeOut", + duration: 2000, + }); + }) + .withOptionPhase(async () => { + //Battle Fog Boss + const encounter = globalScene.currentBattle.mysteryEncounter!; + globalScene.arena.trySetWeather(WeatherType.HEAVY_FOG); + //TODO start fog and stuff + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + setEncounterRewards({ + guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], + fillRemaining: true, + }); + await transitionMysteryEncounterIntroVisuals(); + await initBattleWithEnemyConfig(config); + }) + .build(), + ) + .withOption( + MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + new MoveRequirement(DEFOG_MOVES, true), + new AbilityRequirement(DEFOG_ABILITIES, true), + ), + ) + .withDialogue({ + buttonLabel: `${namespace}:option.2.label`, + buttonTooltip: `${namespace}:option.2.tooltip`, + selected: [ + { + text: `${namespace}:option.2.selected`, + }, + ], + }) + .withPreOptionPhase(async () => { + const encounter = globalScene.currentBattle.mysteryEncounter!; + globalScene.tweens.add({ + targets: encounter.misc.fogOverlay, + alpha: 0, + ease: "Sine.easeOut", + duration: 2000, + }); + }) + .withOptionPhase(async () => { + const encounter = globalScene.currentBattle.mysteryEncounter!; + const primary = encounter.options[1].primaryPokemon!; + if (globalScene.currentBattle.waveIndex >= 140) { + setEncounterExp([primary.id], encounter.enemyPartyConfigs![0].pokemonConfigs![0].species.baseExp); + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards({ + guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], + fillRemaining: true, + }); + await transitionMysteryEncounterIntroVisuals(); + await initBattleWithEnemyConfig(config); + } else { + setEncounterExp([primary.id], encounter.enemyPartyConfigs![0].pokemonConfigs![0].species.baseExp); + leaveEncounterWithoutBattle(); + } + }) + .build(), + ) + .withOption( + MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + new MoveRequirement(LIGHT_MOVES, true), + new AbilityRequirement(LIGHT_ABILITIES, true), + ), + ) + .withDialogue({ + buttonLabel: `${namespace}:option.3.label`, + buttonTooltip: `${namespace}:option.3.tooltip`, + selected: [ + { + text: `${namespace}:option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async () => { + const encounter = globalScene.currentBattle.mysteryEncounter!; + globalScene.tweens.add({ + targets: encounter.misc.fogOverlay, + alpha: 0, + ease: "Sine.easeOut", + duration: 2000, + }); + }) + .withOptionPhase(async () => { + //Navigate through the Fog + const encounter = globalScene.currentBattle.mysteryEncounter!; + const primary = encounter.options[2].primaryPokemon!; + globalScene.arena.trySetWeather(WeatherType.HEAVY_FOG); + if (globalScene.currentBattle.waveIndex >= 140) { + setEncounterExp([primary.id], encounter.enemyPartyConfigs![0].pokemonConfigs![0].species.baseExp); + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards({ + guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], + fillRemaining: true, + }); + await transitionMysteryEncounterIntroVisuals(); + await initBattleWithEnemyConfig(config); + } else { + setEncounterRewards({ + guaranteedModifierTiers: [ModifierTier.ULTRA], + fillRemaining: true, + }); + setEncounterExp([primary.id], encounter.enemyPartyConfigs![0].pokemonConfigs![0].species.baseExp); + leaveEncounterWithoutBattle(); + } + }) + .build(), + ) + + .withSimpleOption( + { + buttonLabel: `${namespace}:option.4.label`, + buttonTooltip: `${namespace}:option.4.tooltip`, + selected: [ + { + text: `${namespace}:option.4.selected`, + }, + ], + }, + async () => { + const encounter = globalScene.currentBattle.mysteryEncounter!; + globalScene.tweens.add({ + targets: encounter.misc.fogOverlay, + alpha: 0, + ease: "Sine.easeOut", + duration: 2000, + }); + const pokemon = globalScene.getPlayerPokemon(); //Can we use this? + globalScene.arena.trySetWeather(WeatherType.FOG, pokemon); + + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(true); + return true; + }, + ) + .build(); + +function chooseBoss() { + const biome = globalScene.arena.biomeType; + const wave = globalScene.currentBattle.waveIndex; + const allBiomePokemon = [ + [ + SpeciesId.MACHAMP, + Nature.JOLLY, + 1, + false, + [MoveId.DYNAMIC_PUNCH, MoveId.STONE_EDGE, MoveId.DUAL_CHOP, MoveId.FISSURE], + [], + ], + [ + SpeciesId.GRIMMSNARL, + Nature.ADAMANT, + null, + false, + [MoveId.STONE_EDGE, MoveId.CLOSE_COMBAT, MoveId.IRON_TAIL, MoveId.PLAY_ROUGH], + [{ modifier: generateModifierType(modifierTypes.MICLE_BERRY) as PokemonHeldItemModifierType }], + ], + ]; + const ForestTallGrassPokemon = [ + [ + SpeciesId.LYCANROC, + Nature.JOLLY, + 2, + false, + [MoveId.STONE_EDGE, MoveId.CLOSE_COMBAT, MoveId.IRON_TAIL, MoveId.PLAY_ROUGH], + [], + ], + [ + SpeciesId.ALOLA_RATICATE, + Nature.ADAMANT, + 1, + false, + [MoveId.FALSE_SURRENDER, MoveId.SUCKER_PUNCH, MoveId.PLAY_ROUGH, MoveId.POPULATION_BOMB], + [{ modifier: generateModifierType(modifierTypes.REVIVER_SEED) as PokemonHeldItemModifierType }], + ], + ]; + const SwampLakePokemon = [ + [ + SpeciesId.POLIWRATH, + Nature.NAIVE, + null, + true, + [MoveId.DYNAMIC_PUNCH, MoveId.HYDRO_PUMP, MoveId.DUAL_CHOP, MoveId.HYPNOSIS], + [ + { modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType }, + { modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType }, + ], + ], + ]; + const GraveyardPokemon = [ + [ + SpeciesId.GOLURK, + Nature.ADAMANT, + 2, + false, + [MoveId.EARTHQUAKE, MoveId.POLTERGEIST, MoveId.DYNAMIC_PUNCH, MoveId.STONE_EDGE], + [], + ], + [ + SpeciesId.HONEDGE, + Nature.CAREFUL, + 0, + false, + [MoveId.IRON_HEAD, MoveId.POLTERGEIST, MoveId.SACRED_SWORD, MoveId.SHADOW_SNEAK], + [], + ], + [ + SpeciesId.ZWEILOUS, + Nature.BRAVE, + null, + true, + [MoveId.DRAGON_RUSH, MoveId.CRUNCH, MoveId.GUNK_SHOT, MoveId.SCREECH], + [{ modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, stackCount: 2 }], + ], + ]; + const wave110_140Pokemon = [ + [ + SpeciesId.SCOLIPEDE, + Nature.ADAMANT, + 2, + false, + [MoveId.MEGAHORN, MoveId.NOXIOUS_TORQUE, MoveId.ROLLOUT, MoveId.BANEFUL_BUNKER], + [{ modifier: generateModifierType(modifierTypes.MICLE_BERRY) as PokemonHeldItemModifierType }], + ], + [ + SpeciesId.MIENSHAO, + Nature.JOLLY, + null, + true, + [MoveId.HIGH_JUMP_KICK, MoveId.STONE_EDGE, MoveId.BLAZE_KICK, MoveId.GUNK_SHOT], + [], + ], + [ + SpeciesId.DRACOZOLT, + Nature.JOLLY, + null, + true, + [MoveId.BOLT_BEAK, MoveId.DRAGON_RUSH, MoveId.EARTHQUAKE, MoveId.STONE_EDGE], + [], + ], + ]; + const wave140PlusPokemon = [ + [ + SpeciesId.PIDGEOT, + Nature.HASTY, + 0, + false, + [MoveId.HURRICANE, MoveId.HEAT_WAVE, MoveId.FOCUS_BLAST, MoveId.WILDBOLT_STORM], + [], + ], + ]; + + let pool = allBiomePokemon as [SpeciesId, Nature, AbilityId, boolean, MoveId[], HeldModifierConfig[]][]; + + // Include biome-specific Pokémon if within wave 50-80 + if (wave >= 50) { + if (biome === BiomeId.FOREST || biome === BiomeId.TALL_GRASS) { + pool = pool.concat( + ForestTallGrassPokemon as [SpeciesId, Nature, AbilityId, boolean, MoveId[], HeldModifierConfig[]][], + ); + } + if (biome === BiomeId.SWAMP || biome === BiomeId.LAKE) { + pool = pool.concat(SwampLakePokemon as [SpeciesId, Nature, AbilityId, boolean, MoveId[], HeldModifierConfig[]][]); + } + if (biome === BiomeId.GRAVEYARD) { + pool = pool.concat(GraveyardPokemon as [SpeciesId, Nature, AbilityId, boolean, MoveId[], HeldModifierConfig[]][]); + } + } + + // Waves 110-140 content + if (wave >= 110) { + pool = pool.concat(wave110_140Pokemon as [SpeciesId, Nature, AbilityId, boolean, MoveId[], HeldModifierConfig[]][]); + } + + // Wave 140+ + if (wave >= 140) { + pool = pool.concat(wave140PlusPokemon as [SpeciesId, Nature, AbilityId, boolean, MoveId[], HeldModifierConfig[]][]); + } + // Randomly choose one + return pool[randSeedInt(pool.length, 0)]; +} diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 5ee289a6c56..53f9a9521bc 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -32,6 +32,7 @@ import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fu import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter"; +import { CreepingFogEncounter } from "#app/data/mystery-encounters/encounters/creeping-fog-encounter"; import { getBiomeName } from "#app/data/balance/biomes"; export const EXTREME_ENCOUNTER_BIOMES = [ @@ -192,11 +193,11 @@ export const mysteryEncountersByBiome = new Map [BiomeId.TOWN, []], [BiomeId.PLAINS, [MysteryEncounterType.SLUMBERING_SNORLAX]], [BiomeId.GRASS, [MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.ABSOLUTE_AVARICE]], - [BiomeId.TALL_GRASS, [MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.ABSOLUTE_AVARICE]], + [BiomeId.TALL_GRASS, [MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.ABSOLUTE_AVARICE, MysteryEncounterType.CREEPING_FOG]], [BiomeId.METROPOLIS, []], - [BiomeId.FOREST, [MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.ABSOLUTE_AVARICE]], + [BiomeId.FOREST, [MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.ABSOLUTE_AVARICE, MysteryEncounterType.CREEPING_FOG]], [BiomeId.SEA, [MysteryEncounterType.LOST_AT_SEA]], - [BiomeId.SWAMP, [MysteryEncounterType.SAFARI_ZONE]], + [BiomeId.SWAMP, [MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.CREEPING_FOG]], [BiomeId.BEACH, []], [BiomeId.LAKE, []], [BiomeId.SEABED, []], @@ -208,7 +209,7 @@ export const mysteryEncountersByBiome = new Map [BiomeId.MEADOW, []], [BiomeId.POWER_PLANT, []], [BiomeId.VOLCANO, [MysteryEncounterType.FIERY_FALLOUT, MysteryEncounterType.DANCING_LESSONS]], - [BiomeId.GRAVEYARD, []], + [BiomeId.GRAVEYARD, [MysteryEncounterType.CREEPING_FOG]], [BiomeId.DOJO, []], [BiomeId.FACTORY, []], [BiomeId.RUINS, []], @@ -257,6 +258,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter; + allMysteryEncounters[MysteryEncounterType.CREEPING_FOG] = CreepingFogEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/data/mystery-encounters/requirements/requirement-groups.ts b/src/data/mystery-encounters/requirements/requirement-groups.ts index 0140a5fe320..4e4b78b7457 100644 --- a/src/data/mystery-encounters/requirements/requirement-groups.ts +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -108,6 +108,16 @@ export const EXTORTION_MOVES = [ MoveId.STRING_SHOT, ]; +/** + * Moves that can clear a foggy weather + */ +export const DEFOG_MOVES = [MoveId.DEFOG, MoveId.RAPID_SPIN, MoveId.GUST]; + +/** + * Moves that can help navigate through foggy weather + */ +export const LIGHT_MOVES = [MoveId.FLASH, MoveId.FORESIGHT]; + /** * Abilities that (loosely) can be used to trap/rob someone */ @@ -135,3 +145,18 @@ export const FIRE_RESISTANT_ABILITIES = [ AbilityId.STEAM_ENGINE, AbilityId.PRIMORDIAL_SEA, ]; + +/** + * Abilities that can clear foggy weather + */ +export const DEFOG_ABILITIES = [AbilityId.AIR_LOCK, AbilityId.CLOUD_NINE]; + +/** + * Abilities that can help navigate through foggy weather + */ +export const LIGHT_ABILITIES = [ + AbilityId.KEEN_EYE, + AbilityId.ILLUMINATE, + AbilityId.COMPOUND_EYES, + AbilityId.VICTORY_STAR, +]; diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index ea129454034..efb3c52e840 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -256,9 +256,9 @@ export const pokemonFormChanges: PokemonFormChanges = { new SpeciesFormChange(SpeciesId.CASTFORM, "", "snowy", new SpeciesFormChangeWeatherTrigger(AbilityId.FORECAST, [ WeatherType.HAIL, WeatherType.SNOW ]), true), new SpeciesFormChange(SpeciesId.CASTFORM, "sunny", "snowy", new SpeciesFormChangeWeatherTrigger(AbilityId.FORECAST, [ WeatherType.HAIL, WeatherType.SNOW ]), true), new SpeciesFormChange(SpeciesId.CASTFORM, "rainy", "snowy", new SpeciesFormChangeWeatherTrigger(AbilityId.FORECAST, [ WeatherType.HAIL, WeatherType.SNOW ]), true), - new SpeciesFormChange(SpeciesId.CASTFORM, "sunny", "", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]), true), - new SpeciesFormChange(SpeciesId.CASTFORM, "rainy", "", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]), true), - new SpeciesFormChange(SpeciesId.CASTFORM, "snowy", "", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]), true), + new SpeciesFormChange(SpeciesId.CASTFORM, "sunny", "", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HEAVY_FOG ]), true), + new SpeciesFormChange(SpeciesId.CASTFORM, "rainy", "", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HEAVY_FOG ]), true), + new SpeciesFormChange(SpeciesId.CASTFORM, "snowy", "", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HEAVY_FOG ]), true), new SpeciesFormChange(SpeciesId.CASTFORM, "sunny", "", new SpeciesFormChangeActiveTrigger(), true), new SpeciesFormChange(SpeciesId.CASTFORM, "rainy", "", new SpeciesFormChangeActiveTrigger(), true), new SpeciesFormChange(SpeciesId.CASTFORM, "snowy", "", new SpeciesFormChangeActiveTrigger(), true) @@ -300,7 +300,7 @@ export const pokemonFormChanges: PokemonFormChanges = { ], [SpeciesId.CHERRIM]: [ new SpeciesFormChange(SpeciesId.CHERRIM, "overcast", "sunshine", new SpeciesFormChangeWeatherTrigger(AbilityId.FLOWER_GIFT, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]), true), - new SpeciesFormChange(SpeciesId.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]), true), + new SpeciesFormChange(SpeciesId.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeRevertWeatherFormTrigger(AbilityId.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HEAVY_FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]), true), new SpeciesFormChange(SpeciesId.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeActiveTrigger(), true) ], [SpeciesId.LOPUNNY]: [ diff --git a/src/data/weather.ts b/src/data/weather.ts index 425e15b12a8..27178de70b7 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -37,6 +37,7 @@ export class Weather { case WeatherType.HEAVY_RAIN: case WeatherType.HARSH_SUN: case WeatherType.STRONG_WINDS: + case WeatherType.HEAVY_FOG: return true; } @@ -137,6 +138,8 @@ export function getWeatherStartMessage(weatherType: WeatherType): string | null return i18next.t("weather:snowStartMessage"); case WeatherType.FOG: return i18next.t("weather:fogStartMessage"); + case WeatherType.HEAVY_FOG: + return i18next.t("weather:heavyFogStartMessage"); case WeatherType.HEAVY_RAIN: return i18next.t("weather:heavyRainStartMessage"); case WeatherType.HARSH_SUN: @@ -162,6 +165,8 @@ export function getWeatherLapseMessage(weatherType: WeatherType): string | null return i18next.t("weather:snowLapseMessage"); case WeatherType.FOG: return i18next.t("weather:fogLapseMessage"); + case WeatherType.HEAVY_FOG: + return i18next.t("weather:heavyFogLapseMessage"); case WeatherType.HEAVY_RAIN: return i18next.t("weather:heavyRainLapseMessage"); case WeatherType.HARSH_SUN: @@ -202,6 +207,8 @@ export function getWeatherClearMessage(weatherType: WeatherType): string | null return i18next.t("weather:snowClearMessage"); case WeatherType.FOG: return i18next.t("weather:fogClearMessage"); + case WeatherType.HEAVY_FOG: + return i18next.t("weather:fogClearMessage"); case WeatherType.HEAVY_RAIN: return i18next.t("weather:heavyRainClearMessage"); case WeatherType.HARSH_SUN: @@ -221,6 +228,8 @@ export function getLegendaryWeatherContinuesMessage(weatherType: WeatherType): s return i18next.t("weather:heavyRainContinueMessage"); case WeatherType.STRONG_WINDS: return i18next.t("weather:strongWindsContinueMessage"); + case WeatherType.HEAVY_FOG: + return i18next.t("weather:heavyFogContinueMessage"); } return null; } diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index b973652b113..79feeecbbc6 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -29,5 +29,6 @@ export enum MysteryEncounterType { FUN_AND_GAMES, UNCOMMON_BREED, GLOBAL_TRADE_SYSTEM, - THE_EXPERT_POKEMON_BREEDER + THE_EXPERT_POKEMON_BREEDER, + CREEPING_FOG } diff --git a/src/enums/weather-type.ts b/src/enums/weather-type.ts index fa699bb3514..d93d56e3318 100644 --- a/src/enums/weather-type.ts +++ b/src/enums/weather-type.ts @@ -9,4 +9,5 @@ export enum WeatherType { HEAVY_RAIN, HARSH_SUN, STRONG_WINDS, + HEAVY_FOG, } diff --git a/src/field/arena.ts b/src/field/arena.ts index 8d7e5037852..5ac11e0079a 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -327,8 +327,15 @@ export class Arena { if ( this.weather?.isImmutable() && - ![WeatherType.HARSH_SUN, WeatherType.HEAVY_RAIN, WeatherType.STRONG_WINDS, WeatherType.NONE].includes(weather) + ![ + WeatherType.HARSH_SUN, + WeatherType.HEAVY_RAIN, + WeatherType.STRONG_WINDS, + WeatherType.HEAVY_FOG, + WeatherType.NONE, + ].includes(weather) ) { + if (oldWeatherType !== WeatherType.HEAVY_FOG) { globalScene.phaseManager.unshiftNew( "CommonAnimPhase", undefined, @@ -336,6 +343,7 @@ export class Arena { CommonAnim.SUNNY + (oldWeatherType - 1), true, ); + } globalScene.phaseManager.queueMessage(getLegendaryWeatherContinuesMessage(oldWeatherType)!); return false; } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index f67d19e1027..a82de228a52 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -236,6 +236,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("pb", ""); this.loadAtlas("items", ""); + this.loadImage("heavy_fog", ""); this.loadAtlas("types", ""); // Get current lang and load the types atlas for it. English will only load types while all other languages will load types and types_ diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index a04a5e2be47..3b4b1c44478 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1147,6 +1147,9 @@ export class PokemonMoveAccuracyBoosterModifierType extends PokemonHeldItemModif } getDescription(): string { + if (this.amount === -1) { + return i18next.t("modifierType:ModifierType.MICLE_BERRY.description"); + } return i18next.t("modifierType:ModifierType.PokemonMoveAccuracyBoosterModifierType.description", { accuracyAmount: this.amount, }); @@ -2141,6 +2144,8 @@ const modifierTypeInitObj = Object.freeze({ GRIP_CLAW: () => new ContactHeldItemTransferChanceModifierType("modifierType:ModifierType.GRIP_CLAW", "grip_claw", 10), WIDE_LENS: () => new PokemonMoveAccuracyBoosterModifierType("modifierType:ModifierType.WIDE_LENS", "wide_lens", 5), + MICLE_BERRY: () => + new PokemonMoveAccuracyBoosterModifierType("modifierType:ModifierType.MICLE_BERRY", "micle_berry", -1), MULTI_LENS: () => new PokemonMultiHitModifierType("modifierType:ModifierType.MULTI_LENS", "zoom_lens"), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 54b7323569a..7a85bda8952 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2739,8 +2739,11 @@ export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier * @returns always `true` */ override apply(_pokemon: Pokemon, moveAccuracy: NumberHolder): boolean { - moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * this.getStackCount(); - + if (this.accuracyAmount !== -1) { + moveAccuracy.value = moveAccuracy.value + this.accuracyAmount * this.getStackCount(); + } else { + moveAccuracy.value = -1; + } return true; } diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 7d3b30ed5b0..5d4294ea04f 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -299,6 +299,7 @@ export async function initI18n(): Promise { "mysteryEncounters/uncommonBreed", "mysteryEncounters/globalTradeSystem", "mysteryEncounters/theExpertPokemonBreeder", + "mysteryEncounters/creepingFog", "mysteryEncounterMessages", ], detection: { diff --git a/src/ui/fog-overlay.ts b/src/ui/fog-overlay.ts new file mode 100644 index 00000000000..2fd0ded4a96 --- /dev/null +++ b/src/ui/fog-overlay.ts @@ -0,0 +1,52 @@ +import { globalScene } from "#app/global-scene"; + +export interface FogOverlaySettings { + delayVisibility?: boolean; + scale?: number; + top?: boolean; + right?: boolean; + onSide?: boolean; + x?: number; + y?: number; + width?: number; + height?: number; +} + +const EFF_HEIGHT = 48; +const EFF_WIDTH = 82; + +export default class FogOverlay extends Phaser.GameObjects.Container { + public active = false; + + private val: Phaser.GameObjects.Container; + private typ: Phaser.GameObjects.Sprite; + + constructor(options?: FogOverlaySettings) { + if (options?.onSide) { + options.top = false; + } + super(globalScene, options?.x, options?.y); + const scale = options?.scale || 1; // set up the scale + this.setScale(scale); + + this.val = new Phaser.GameObjects.Container( + globalScene, + options?.onSide && !options?.right ? EFF_WIDTH : 0, + options?.top ? EFF_HEIGHT : 0, + ); + this.typ = globalScene.add.sprite(25, EFF_HEIGHT - 35, "heavy_fog"); + this.typ.setAlpha(1); + this.setAlpha(0); + this.typ.setScale(0.8); + this.val.add(this.typ); + this.add(this.val); + this.setVisible(false); + } + clear() { + this.setVisible(false); + this.active = false; + } + isActive(): boolean { + return this.active; + } +} diff --git a/test/mystery-encounter/encounters/creeping-fog-encounter.test.ts b/test/mystery-encounter/encounters/creeping-fog-encounter.test.ts new file mode 100644 index 00000000000..753c4eae193 --- /dev/null +++ b/test/mystery-encounter/encounters/creeping-fog-encounter.test.ts @@ -0,0 +1,372 @@ +import type BattleScene from "#app/battle-scene"; +import { CreepingFogEncounter } from "#app/data/mystery-encounters/encounters/creeping-fog-encounter"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { BiomeId } from "#enums/biome-id"; +import { TimeOfDay } from "#enums/time-of-day"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { SpeciesId } from "#enums/species-id"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; +import { ModifierTier } from "#enums/modifier-tier"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { UiMode } from "#enums/ui-mode"; +import { MoveId } from "#enums/move-id"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { + runMysteryEncounterToEnd, + runSelectMysteryEncounterOption, + skipBattleRunMysteryEncounterRewardsPhase, +} from "#test/mystery-encounter/encounter-test-utils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import GameManager from "#test/testUtils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { WeatherType } from "#enums/weather-type"; + +const namespace = "mysteryEncounters/creepingFog"; +const defaultParty = [SpeciesId.LAPRAS, SpeciesId.GENGAR, SpeciesId.ABRA]; +const defaultBiome = BiomeId.FOREST; +const defaultWave = 51; +const enemyPokemonForest50_110 = [ + SpeciesId.MACHAMP, + SpeciesId.GRIMMSNARL, + SpeciesId.LYCANROC, + SpeciesId.ALOLA_RATICATE, +]; +const enemyPokemonSwamp110_140 = [ + SpeciesId.MACHAMP, + SpeciesId.GRIMMSNARL, + SpeciesId.POLIWRATH, + SpeciesId.SCOLIPEDE, + SpeciesId.MIENSHAO, + SpeciesId.DRACOZOLT, +]; +const enemyPokemonGraveyard140_Plus = [ + SpeciesId.MACHAMP, + SpeciesId.GRIMMSNARL, + SpeciesId.GOLURK, + SpeciesId.HONEDGE, + SpeciesId.ZWEILOUS, + SpeciesId.SCOLIPEDE, + SpeciesId.MIENSHAO, + SpeciesId.DRACOZOLT, + SpeciesId.PIDGEOT, +]; + +const enemyMoveset = { + [SpeciesId.MACHAMP]: [MoveId.DYNAMIC_PUNCH, MoveId.STONE_EDGE, MoveId.DUAL_CHOP, MoveId.FISSURE], + [SpeciesId.GRIMMSNARL]: [MoveId.STONE_EDGE, MoveId.CLOSE_COMBAT, MoveId.IRON_TAIL, MoveId.PLAY_ROUGH], + [SpeciesId.LYCANROC]: [MoveId.STONE_EDGE, MoveId.CLOSE_COMBAT, MoveId.IRON_TAIL, MoveId.PLAY_ROUGH], + [SpeciesId.ALOLA_RATICATE]: [MoveId.FALSE_SURRENDER, MoveId.SUCKER_PUNCH, MoveId.PLAY_ROUGH, MoveId.POPULATION_BOMB], + [SpeciesId.POLIWRATH]: [MoveId.DYNAMIC_PUNCH, MoveId.HYDRO_PUMP, MoveId.DUAL_CHOP, MoveId.HYPNOSIS], + [SpeciesId.GOLURK]: [MoveId.EARTHQUAKE, MoveId.POLTERGEIST, MoveId.DYNAMIC_PUNCH, MoveId.STONE_EDGE], + [SpeciesId.HONEDGE]: [MoveId.IRON_HEAD, MoveId.POLTERGEIST, MoveId.SACRED_SWORD, MoveId.SHADOW_SNEAK], + [SpeciesId.ZWEILOUS]: [MoveId.DRAGON_RUSH, MoveId.CRUNCH, MoveId.GUNK_SHOT, MoveId.SCREECH], + [SpeciesId.SCOLIPEDE]: [MoveId.MEGAHORN, MoveId.NOXIOUS_TORQUE, MoveId.ROLLOUT, MoveId.BANEFUL_BUNKER], + [SpeciesId.MIENSHAO]: [MoveId.HIGH_JUMP_KICK, MoveId.STONE_EDGE, MoveId.BLAZE_KICK, MoveId.GUNK_SHOT], + [SpeciesId.DRACOZOLT]: [MoveId.BOLT_BEAK, MoveId.DRAGON_RUSH, MoveId.EARTHQUAKE, MoveId.STONE_EDGE], + [SpeciesId.PIDGEOT]: [MoveId.HURRICANE, MoveId.HEAT_WAVE, MoveId.FOCUS_BLAST, MoveId.WILDBOLT_STORM], +}; + +describe("Creeping Fog - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + game.override.startingTimeOfDay(TimeOfDay.NIGHT); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [BiomeId.FOREST, [MysteryEncounterType.CREEPING_FOG]], + [BiomeId.FOREST, [MysteryEncounterType.SAFARI_ZONE]], + [BiomeId.SPACE, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]), + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + + expect(CreepingFogEncounter.encounterType).toBe(MysteryEncounterType.CREEPING_FOG); + expect(CreepingFogEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(CreepingFogEncounter.dialogue).toBeDefined(); + expect(CreepingFogEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}:intro` }]); + expect(CreepingFogEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); + expect(CreepingFogEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`); + expect(CreepingFogEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`); + expect(CreepingFogEncounter.options.length).toBe(4); + }); + + it("should not spawn outside of proper biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA); + game.override.startingBiome(BiomeId.SPACE); + game.override.startingTimeOfDay(TimeOfDay.NIGHT); + + await game.runToMysteryEncounter(); + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CREEPING_FOG); + }); + + it("should not spawn outside of proper time of day", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA); + game.override.startingTimeOfDay(TimeOfDay.DAY); + + await game.runToMysteryEncounter(); + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CREEPING_FOG); + }); + + describe("Option 1 - Confront the shadow", () => { + it("should have the correct properties", () => { + const option1 = CreepingFogEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option.1.label`, + buttonTooltip: `${namespace}:option.1.tooltip`, + selected: [ + { + text: `${namespace}:option.1.selected`, + }, + ], + }); + }); + + it("should start battle against shadowy Pokemon from the Forest Low Level Pool", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + const partyLead = scene.getPlayerParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + //Expect that the weather is set to heavy fog + expect(scene.arena.weather?.weatherType).toBe(WeatherType.HEAVY_FOG); + const enemyField = scene.getEnemyField(); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyPokemonForest50_110).toContain(enemyField[0].species.speciesId); + const moveset = enemyField[0].moveset.map(m => m.moveId); + expect(enemyMoveset[enemyField[0].species.speciesId]).toEqual(moveset); + }); + + it("should start battle against shadowy Pokemon from the Swamp Mid Level Pool", async () => { + game.override.startingWave(113); + game.override.startingBiome(BiomeId.SWAMP); + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + // Make party lead's level arbitrarily high to not get KOed by move + const partyLead = scene.getPlayerParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + //Expect that the weather is set to heavy fog + expect(scene.arena.weather?.weatherType).toBe(WeatherType.HEAVY_FOG); + const enemyField = scene.getEnemyField(); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyPokemonSwamp110_140).toContain(enemyField[0].species.speciesId); + const moveset = enemyField[0].moveset.map(m => m.moveId); + expect(enemyMoveset[enemyField[0].species.speciesId]).toEqual(moveset); + }); + + it("should start battle against shadowy Pokemon from the Graveyard High Level Pool", async () => { + game.override.startingWave(143); + game.override.startingBiome(BiomeId.GRAVEYARD); + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + // Make party lead's level arbitrarily high to not get KOed by move + const partyLead = scene.getPlayerParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + //Expect that the weather is set to heavy fog + expect(scene.arena.weather?.weatherType).toBe(WeatherType.HEAVY_FOG); + const enemyField = scene.getEnemyField(); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyPokemonGraveyard140_Plus).toContain(enemyField[0].species.speciesId); + const moveset = enemyField[0].moveset.map(m => m.moveId); + expect(enemyMoveset[enemyField[0].species.speciesId]).toEqual(moveset); + }); + + it("should have a 2 rogue tier items in the rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find( + h => h instanceof ModifierSelectUiHandler, + ) as ModifierSelectUiHandler; + expect( + modifierSelectHandler.options[0].modifierTypeOption.type.tier - + modifierSelectHandler.options[0].modifierTypeOption.upgradeCount, + ).toEqual(ModifierTier.ROGUE); + expect( + modifierSelectHandler.options[1].modifierTypeOption.type.tier - + modifierSelectHandler.options[1].modifierTypeOption.upgradeCount, + ).toEqual(ModifierTier.ROGUE); + }); + }); + + describe("Option 2 - Clear the Fog", () => { + it("should have the correct properties", () => { + const option2 = CreepingFogEncounter.options[1]; + expect(option2.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option2.dialogue).toBeDefined(); + expect(option2.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option.2.label`, + buttonTooltip: `${namespace}:option.2.tooltip`, + selected: [ + { + text: `${namespace}:option.2.selected`, + }, + ], + }); + }); + + it("should skip battle with Pokemon if wave level under 140", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + scene.getPlayerParty()[1].moveset = [new PokemonMove(MoveId.DEFOG)]; + await runMysteryEncounterToEnd(game, 2); + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should not skip battle with Pokemon", async () => { + game.override.startingWave(143); + game.override.startingBiome(BiomeId.GRAVEYARD); + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + scene.getPlayerParty()[1].moveset = [new PokemonMove(MoveId.DEFOG)]; + const partyLead = scene.getPlayerParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 2, undefined, true); + const enemyField = scene.getEnemyField(); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyPokemonGraveyard140_Plus).toContain(enemyField[0].species.speciesId); + const moveset = enemyField[0].moveset.map(m => m.moveId); + expect(enemyMoveset[enemyField[0].species.speciesId]).toEqual(moveset); + }); + + it("should NOT be selectable if the player doesn't have a defog type move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + scene.getPlayerParty().forEach(p => (p.moveset = [])); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + const encounterPhase = scene.phaseManager.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + await runSelectMysteryEncounterOption(game, 2); + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + }); + + describe("Option 3 - Navigate through the Fog", () => { + it("should have the correct properties", () => { + const option3 = CreepingFogEncounter.options[2]; + expect(option3.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option3.dialogue).toBeDefined(); + expect(option3.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option.3.label`, + buttonTooltip: `${namespace}:option.3.tooltip`, + selected: [ + { + text: `${namespace}:option.3.selected`, + }, + ], + }); + }); + + it("should skip battle with Pokemon if wave level under 140", async () => { + game.override.startingWave(63); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + scene.getPlayerParty()[1].moveset = [new PokemonMove(MoveId.FORESIGHT)]; + await runMysteryEncounterToEnd(game, 3); + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should not skip battle with Pokemon", async () => { + game.override.startingWave(143); + game.override.startingBiome(BiomeId.GRAVEYARD); + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + scene.getPlayerParty()[1].moveset = [new PokemonMove(MoveId.FORESIGHT)]; + const partyLead = scene.getPlayerParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 3, undefined, true); + //Expect that the weather is set to heavy fog + expect(scene.arena.weather?.weatherType).toBe(WeatherType.HEAVY_FOG); + const enemyField = scene.getEnemyField(); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyPokemonGraveyard140_Plus).toContain(enemyField[0].species.speciesId); + const moveset = enemyField[0].moveset.map(m => m.moveId); + expect(enemyMoveset[enemyField[0].species.speciesId]).toEqual(moveset); + }); + + it("should NOT be selectable if the player doesn't have a light type move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + scene.getPlayerParty().forEach(p => (p.moveset = [])); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.phaseManager.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + + await runSelectMysteryEncounterOption(game, 3); + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + }); + + describe("Option 4 - Leave the encounter", () => { + it("should have the correct properties", () => { + const option4 = CreepingFogEncounter.options[3]; + expect(option4.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option4.dialogue).toBeDefined(); + expect(option4.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option.4.label`, + buttonTooltip: `${namespace}:option.4.tooltip`, + selected: [ + { + text: `${namespace}:option.4.selected`, + }, + ], + }); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty); + await runMysteryEncounterToEnd(game, 4); + //Expect that the weather is set to fog + expect(scene.arena.weather?.weatherType).toBe(WeatherType.FOG); + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/test/testUtils/helpers/overridesHelper.ts b/test/testUtils/helpers/overridesHelper.ts index 3bf0fbbda47..a9c0c7c2511 100644 --- a/test/testUtils/helpers/overridesHelper.ts +++ b/test/testUtils/helpers/overridesHelper.ts @@ -17,6 +17,7 @@ import { GameManagerHelper } from "./gameManagerHelper"; import { coerceArray, shiftCharCodes } from "#app/utils/common"; import type { RandomTrainerOverride } from "#app/overrides"; import type { BattleType } from "#enums/battle-type"; +import type { TimeOfDay } from "#enums/time-of-day"; /** * Helper to handle overrides in tests @@ -38,6 +39,17 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Override the starting time of day + * @param timeOfDay - The time of day to be set + * @returns `this` + */ + public startingTimeOfDay(timeOfDay: TimeOfDay): this { + vi.spyOn(Overrides, "ARENA_TINT_OVERRIDE", "get").mockReturnValue(timeOfDay); + this.log(`Starting time of day set to ${timeOfDay}!`); + return this; + } + /** * Override the starting wave index * @param wave - The wave to set. Classic: `1`-`200`