Implement Creeping Fog Mystery Encounter Feature

This Mystery Encounter is a random event that can
occur from wave 50 and above that causes a fog
to appear covering the screen and a wild pokemon
to be hidden underneath it. The player when
facing this encounter may choose one of four
options which includes fighting, using a light
based move or ability to light the way, using a
defog move or ability to clear the fog, or
waiting for the fog to thin out. Depending on
your choice the player might be granted more
experience and better rewards.
The pokemon encountered changes dynamically with
the biome and stage level that the Mystery
Encounter occurs. Changes made:
- Implemented new mystery encounter called
  "Creeping Fog" ("creeping-fog-encounter.ts")
  and added a new corresponding weather type
  called "Heavy Fog" ("weather.ts").
- Updated corresponding weather functions
  and fog affected pokemon functions.
  ("move.ts", "pokemon-forms.ts", "ability.ts",
  "pokemon-evolutions.ts").
- Created new Fog Overlay for the mystery
  encounter ("fog-overlay.ts").
- Created new item called "Micle Berry" to
  grant perfect accuracy ("modifier-type.ts").
- Created Mystery Encounter Creeping Fog unit
  test ("creeping-fog-encounter.test.ts").
- Created new Mystery Encounter Light and Defog
  moves and abilities requirements
  ("requirements-groups.ts").
Signed-off-by: Fuad Ali <fuad.ali@tecnico.ulisboa.pt>
Co-authored-by: Matilde Simões <matilde.simoes@tecnico.ulisboa.pt>
This commit is contained in:
Fuad Ali 2025-05-22 20:07:08 +01:00
parent ff9aefb0e5
commit 2264a28300
22 changed files with 980 additions and 22 deletions

BIN
public/images/heavy_fog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

View File

@ -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
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

View File

@ -8111,7 +8111,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()
@ -8299,7 +8299,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(),
@ -9003,7 +9003,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()

View File

@ -1138,7 +1138,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.SLIGGOO, 40, null, new TimeOfDayEvolutionCondition("day"))
],
[SpeciesId.SLIGGOO]: [
new SpeciesEvolution(SpeciesId.GOODRA, 50, null, new WeatherEvolutionCondition([ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]), SpeciesWildEvolutionDelay.LONG)
new SpeciesEvolution(SpeciesId.GOODRA, 50, null, new WeatherEvolutionCondition([ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_FOG, WeatherType.HEAVY_RAIN ]), SpeciesWildEvolutionDelay.LONG)
],
[SpeciesId.BERGMITE]: [
new SpeciesEvolution(SpeciesId.HISUI_AVALUGG, 37, null, new TimeOfDayEvolutionCondition("night")),
@ -1361,7 +1361,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.HISUI_ZOROARK, 30, null, null)
],
[SpeciesId.HISUI_SLIGGOO]: [
new SpeciesEvolution(SpeciesId.HISUI_GOODRA, 50, null, new WeatherEvolutionCondition([ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ]), SpeciesWildEvolutionDelay.LONG)
new SpeciesEvolution(SpeciesId.HISUI_GOODRA, 50, null, new WeatherEvolutionCondition([ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_FOG, WeatherType.HEAVY_RAIN ]), SpeciesWildEvolutionDelay.LONG)
],
[SpeciesId.SPRIGATITO]: [
new SpeciesEvolution(SpeciesId.FLORAGATO, 16, null, null)

View File

@ -768,9 +768,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");
@ -778,7 +775,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}
@ -9365,7 +9366,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;
}

View File

@ -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/modifier/modifier-type";
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 { Moves } from "#enums/moves";
import { Species } from "#enums/species";
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 { Abilities } from "#enums/abilities";
import { Stat } from "#enums/stat";
import type HeldModifierConfig from "#app/interfaces/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 "#app/modifier/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 { Biome } from "#enums/biome";
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 encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/4423 | 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 Species;
const naturePokemon = chosenPokemonAttributes[1] as Nature;
const abilityPokemon = chosenPokemonAttributes[2] as Abilities;
const passivePokemon = chosenPokemon[3] as boolean;
const movesPokemon = chosenPokemonAttributes[4] as Moves[];
const modifPokemon = chosenPokemonAttributes[5] as HeldModifierConfig[];
const segments = waveIndex < 80 ? 2 : waveIndex < 140 ? 3 : 4;
const pokemonConfig: EnemyPokemonConfig = {
species: getPokemonSpecies(chosenPokemon),
formIndex: [Species.LYCANROC, Species.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 = [
[
Species.MACHAMP,
Nature.JOLLY,
1,
false,
[Moves.DYNAMIC_PUNCH, Moves.STONE_EDGE, Moves.DUAL_CHOP, Moves.FISSURE],
[],
],
[
Species.GRIMMSNARL,
Nature.ADAMANT,
null,
false,
[Moves.STONE_EDGE, Moves.CLOSE_COMBAT, Moves.IRON_TAIL, Moves.PLAY_ROUGH],
[{ modifier: generateModifierType(modifierTypes.MICLE_BERRY) as PokemonHeldItemModifierType }],
],
];
const ForestTallGrassPokemon = [
[
Species.LYCANROC,
Nature.JOLLY,
2,
false,
[Moves.STONE_EDGE, Moves.CLOSE_COMBAT, Moves.IRON_TAIL, Moves.PLAY_ROUGH],
[],
],
[
Species.ALOLA_RATICATE,
Nature.ADAMANT,
1,
false,
[Moves.FALSE_SURRENDER, Moves.SUCKER_PUNCH, Moves.PLAY_ROUGH, Moves.POPULATION_BOMB],
[{ modifier: generateModifierType(modifierTypes.REVIVER_SEED) as PokemonHeldItemModifierType }],
],
];
const SwampLakePokemon = [
[
Species.POLIWRATH,
Nature.NAIVE,
null,
true,
[Moves.DYNAMIC_PUNCH, Moves.HYDRO_PUMP, Moves.DUAL_CHOP, Moves.HYPNOSIS],
[
{ modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType },
{ modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType },
],
],
];
const GraveyardPokemon = [
[
Species.GOLURK,
Nature.ADAMANT,
2,
false,
[Moves.EARTHQUAKE, Moves.POLTERGEIST, Moves.DYNAMIC_PUNCH, Moves.STONE_EDGE],
[],
],
[
Species.HONEDGE,
Nature.CAREFUL,
0,
false,
[Moves.IRON_HEAD, Moves.POLTERGEIST, Moves.SACRED_SWORD, Moves.SHADOW_SNEAK],
[],
],
[
Species.ZWEILOUS,
Nature.BRAVE,
null,
true,
[Moves.DRAGON_RUSH, Moves.CRUNCH, Moves.GUNK_SHOT, Moves.SCREECH],
[{ modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, stackCount: 2 }],
],
];
const wave110_140Pokemon = [
[
Species.SCOLIPEDE,
Nature.ADAMANT,
2,
false,
[Moves.MEGAHORN, Moves.NOXIOUS_TORQUE, Moves.ROLLOUT, Moves.BANEFUL_BUNKER],
[{ modifier: generateModifierType(modifierTypes.MICLE_BERRY) as PokemonHeldItemModifierType }],
],
[
Species.MIENSHAO,
Nature.JOLLY,
null,
true,
[Moves.HIGH_JUMP_KICK, Moves.STONE_EDGE, Moves.BLAZE_KICK, Moves.GUNK_SHOT],
[],
],
[
Species.DRACOZOLT,
Nature.JOLLY,
null,
true,
[Moves.BOLT_BEAK, Moves.DRAGON_RUSH, Moves.EARTHQUAKE, Moves.STONE_EDGE],
[],
],
];
const wave140PlusPokemon = [
[
Species.PIDGEOT,
Nature.HASTY,
0,
false,
[Moves.HURRICANE, Moves.HEAT_WAVE, Moves.FOCUS_BLAST, Moves.WILDBOLT_STORM],
[],
],
];
let pool = allBiomePokemon as [Species, Nature, Abilities, boolean, Moves[], HeldModifierConfig[]][];
// Include biome-specific Pokémon if within wave 50-80
if (wave >= 50) {
if (biome === Biome.FOREST || biome === Biome.TALL_GRASS) {
pool = pool.concat(
ForestTallGrassPokemon as [Species, Nature, Abilities, boolean, Moves[], HeldModifierConfig[]][],
);
}
if (biome === Biome.SWAMP || biome === Biome.LAKE) {
pool = pool.concat(SwampLakePokemon as [Species, Nature, Abilities, boolean, Moves[], HeldModifierConfig[]][]);
}
if (biome === Biome.GRAVEYARD) {
pool = pool.concat(GraveyardPokemon as [Species, Nature, Abilities, boolean, Moves[], HeldModifierConfig[]][]);
}
}
// Waves 110-140 content
if (wave >= 110) {
pool = pool.concat(wave110_140Pokemon as [Species, Nature, Abilities, boolean, Moves[], HeldModifierConfig[]][]);
}
// Wave 140+
if (wave >= 140) {
pool = pool.concat(wave140PlusPokemon as [Species, Nature, Abilities, boolean, Moves[], HeldModifierConfig[]][]);
}
// Randomly choose one
return pool[randSeedInt(pool.length, 0)];
}

View File

@ -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, MysteryEncounterType[]>
[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, MysteryEncounterType[]>
[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 => {

View File

@ -108,6 +108,16 @@ export const EXTORTION_MOVES = [
MoveId.STRING_SHOT,
];
/**
* Moves that can clear a foggy weather
*/
export const DEFOG_MOVES = [Moves.DEFOG, Moves.RAPID_SPIN, Moves.GUST];
/**
* Moves that can help navigate through foggy weather
*/
export const LIGHT_MOVES = [Moves.FLASH, Moves.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 = [Abilities.AIR_LOCK, Abilities.CLOUD_NINE];
/**
* Abilities that can help navigate through foggy weather
*/
export const LIGHT_ABILITIES = [
Abilities.KEEN_EYE,
Abilities.ILLUMINATE,
Abilities.COMPOUND_EYES,
Abilities.VICTORY_STAR,
];

View File

@ -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]: [

View File

@ -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;
}

View File

@ -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
}

View File

@ -9,4 +9,5 @@ export enum WeatherType {
HEAVY_RAIN,
HARSH_SUN,
STRONG_WINDS,
HEAVY_FOG,
}

View File

@ -326,8 +326,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,
@ -335,6 +342,7 @@ export class Arena {
CommonAnim.SUNNY + (oldWeatherType - 1),
true,
);
}
globalScene.phaseManager.queueMessage(getLegendaryWeatherContinuesMessage(oldWeatherType)!);
return false;
}

View File

@ -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_<lang>

View File

@ -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,
});
@ -2158,6 +2161,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"),

View File

@ -2759,8 +2759,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;
}

View File

@ -280,6 +280,7 @@ export async function initI18n(): Promise<void> {
"mysteryEncounters/uncommonBreed",
"mysteryEncounters/globalTradeSystem",
"mysteryEncounters/theExpertPokemonBreeder",
"mysteryEncounters/creepingFog",
"mysteryEncounterMessages",
],
detection: {

52
src/ui/fog-overlay.ts Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,367 @@
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 { Biome } from "#app/enums/biome";
import { TimeOfDay } from "#enums/time-of-day";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import { PokemonMove } from "#app/field/pokemon";
import { ModifierTier } from "#app/modifier/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 { Moves } from "#enums/moves";
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 = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.FOREST;
const defaultWave = 51;
const enemyPokemonForest50_110 = [Species.MACHAMP, Species.GRIMMSNARL, Species.LYCANROC, Species.ALOLA_RATICATE];
const enemyPokemonSwamp110_140 = [
Species.MACHAMP,
Species.GRIMMSNARL,
Species.POLIWRATH,
Species.SCOLIPEDE,
Species.MIENSHAO,
Species.DRACOZOLT,
];
const enemyPokemonGraveyard140_Plus = [
Species.MACHAMP,
Species.GRIMMSNARL,
Species.GOLURK,
Species.HONEDGE,
Species.ZWEILOUS,
Species.SCOLIPEDE,
Species.MIENSHAO,
Species.DRACOZOLT,
Species.PIDGEOT,
];
const enemyMoveset = {
[Species.MACHAMP]: [Moves.DYNAMIC_PUNCH, Moves.STONE_EDGE, Moves.DUAL_CHOP, Moves.FISSURE],
[Species.GRIMMSNARL]: [Moves.STONE_EDGE, Moves.CLOSE_COMBAT, Moves.IRON_TAIL, Moves.PLAY_ROUGH],
[Species.LYCANROC]: [Moves.STONE_EDGE, Moves.CLOSE_COMBAT, Moves.IRON_TAIL, Moves.PLAY_ROUGH],
[Species.ALOLA_RATICATE]: [Moves.FALSE_SURRENDER, Moves.SUCKER_PUNCH, Moves.PLAY_ROUGH, Moves.POPULATION_BOMB],
[Species.POLIWRATH]: [Moves.DYNAMIC_PUNCH, Moves.HYDRO_PUMP, Moves.DUAL_CHOP, Moves.HYPNOSIS],
[Species.GOLURK]: [Moves.EARTHQUAKE, Moves.POLTERGEIST, Moves.DYNAMIC_PUNCH, Moves.STONE_EDGE],
[Species.HONEDGE]: [Moves.IRON_HEAD, Moves.POLTERGEIST, Moves.SACRED_SWORD, Moves.SHADOW_SNEAK],
[Species.ZWEILOUS]: [Moves.DRAGON_RUSH, Moves.CRUNCH, Moves.GUNK_SHOT, Moves.SCREECH],
[Species.SCOLIPEDE]: [Moves.MEGAHORN, Moves.NOXIOUS_TORQUE, Moves.ROLLOUT, Moves.BANEFUL_BUNKER],
[Species.MIENSHAO]: [Moves.HIGH_JUMP_KICK, Moves.STONE_EDGE, Moves.BLAZE_KICK, Moves.GUNK_SHOT],
[Species.DRACOZOLT]: [Moves.BOLT_BEAK, Moves.DRAGON_RUSH, Moves.EARTHQUAKE, Moves.STONE_EDGE],
[Species.PIDGEOT]: [Moves.HURRICANE, Moves.HEAT_WAVE, Moves.FOCUS_BLAST, Moves.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<Biome, MysteryEncounterType[]>([
[Biome.FOREST, [MysteryEncounterType.CREEPING_FOG]],
[Biome.FOREST, [MysteryEncounterType.SAFARI_ZONE]],
[Biome.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(Biome.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.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(Biome.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.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(Biome.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.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.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(Moves.DEFOG)];
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
it("should not skip battle with Pokemon", async () => {
game.override.startingWave(143);
game.override.startingBiome(Biome.GRAVEYARD);
await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty);
scene.getPlayerParty()[1].moveset = [new PokemonMove(Moves.DEFOG)];
const partyLead = scene.getPlayerParty()[0];
partyLead.level = 1000;
partyLead.calculateStats();
await runMysteryEncounterToEnd(game, 2, undefined, true);
const enemyField = scene.getEnemyField();
expect(scene.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.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(Moves.FORESIGHT)];
await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
it("should not skip battle with Pokemon", async () => {
game.override.startingWave(143);
game.override.startingBiome(Biome.GRAVEYARD);
await game.runToMysteryEncounter(MysteryEncounterType.CREEPING_FOG, defaultParty);
scene.getPlayerParty()[1].moveset = [new PokemonMove(Moves.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.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.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();
});
});
});

View File

@ -17,6 +17,7 @@ import { GameManagerHelper } from "./gameManagerHelper";
import { 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`