Compare commits

...

10 Commits

Author SHA1 Message Date
Fuad Ali
da41e94451
Merge 8c01e0264d into 4b70fab608 2025-06-20 00:00:13 -04:00
NightKev
4b70fab608
[Bug] Remove message for Rock Head activation (#6014) 2025-06-19 20:59:55 -07:00
lnuvy
1ff2701964
[Bug] Fix when using arrow keys in Pokédex after catching a Pokémon from mystery event (#6000)
fix: wrap setOverlayMode args in array to mystery-encounter

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-06-19 20:45:54 -04:00
Bertie690
1e306e25b5
[Move] Fixed Chilly Reception displaying message when used virtually
https://github.com/pagefaultgames/pokerogue/pull/5843

* Fixed Chilly Reception displaying message when used virtually

* Fixed lack of message causing Chilly Reception to fail

* Fixed tests

* Reverted bool change + fixed test

* Fixed test
2025-06-19 17:14:05 -07:00
Madmadness65
43aa772603
[UI/UX] Add Pokémon category flavor text to Pokédex (#5957)
* Add Pokémon category flavor text to Pokédex

* Append `_category` to locale entry
2025-06-20 00:04:57 +00:00
damocleas
8c01e0264d
Merge branch 'beta' into beta 2025-06-16 21:34:55 -04:00
Fuad Ali
266eed4d98 Fixed Conflicts With creeping fog ME
Signed-off-by: Fuad Ali <fuad.ali@tecnico.ulisboa.pt>
Co-authored-by: Matilde Simões <matilde.simoes@tecnico.ulisboa.pt>
2025-06-12 23:18:52 +01:00
Fuad Ali
27ccd250c2 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>
2025-06-12 21:33:59 +01:00
Fuad Ali
e407e25392 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>
2025-06-12 21:33:59 +01:00
Fuad Ali
2264a28300 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>
2025-06-12 21:33:59 +01:00
27 changed files with 1126 additions and 99 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

@ -306,13 +306,6 @@ export class BlockRecoilDamageAttr extends AbAttr {
): void {
cancelled.value = true;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]) {
return i18next.t("abilityTriggers:blockRecoilDamage", {
pokemonName: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
});
}
}
/**
@ -8125,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()
@ -8313,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(),
@ -9025,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()

View File

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

View File

@ -93,6 +93,10 @@ import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap
import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
/**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
* Conventionally returns `true` for success and `false` for failure.
*/
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -760,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");
@ -770,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}
@ -1390,18 +1395,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
}
}
/**
* Attribute to display a message before a move is executed.
*/
export class PreMoveMessageAttr extends MoveAttr {
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
/** The message to display or a function returning one */
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
/**
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
* @param message - The message to display before move use, either as a string or a function producing one.
* @remarks
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
* (though the move will still succeed).
*/
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
super();
this.message = message;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const message = typeof this.message === "string"
? this.message as string
: this.message(user, target, move);
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
const message = typeof this.message === "function"
? this.message(user, target, move)
: this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) {
globalScene.phaseManager.queueMessage(message, 500);
return true;
@ -9384,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;
}
@ -11299,7 +11317,11 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition),
new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(PreMoveMessageAttr, (user, _target, _move) =>
// Don't display text if current move phase is follow up (ie move called indirectly)
isVirtual((globalScene.phaseManager.getCurrentPhase() as MovePhase).useMode)
? ""
: i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(ChillyReceptionAttr, true),
new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)

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/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)];
}

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 = [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,
];

View File

@ -751,7 +751,7 @@ export async function catchPokemon(
UiMode.POKEDEX_PAGE,
pokemon.species,
pokemon.formIndex,
attributes,
[attributes],
null,
() => {
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {

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

@ -764,7 +764,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
readonly subLegendary: boolean;
readonly legendary: boolean;
readonly mythical: boolean;
readonly species: string;
public category: string;
readonly growthRate: GrowthRate;
/** The chance (as a decimal) for this Species to be male, or `null` for genderless species */
readonly malePercent: number | null;
@ -778,7 +778,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
subLegendary: boolean,
legendary: boolean,
mythical: boolean,
species: string,
category: string,
type1: PokemonType,
type2: PokemonType | null,
height: number,
@ -829,7 +829,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.subLegendary = subLegendary;
this.legendary = legendary;
this.mythical = mythical;
this.species = species;
this.category = category;
this.growthRate = growthRate;
this.malePercent = malePercent;
this.genderDiffs = genderDiffs;
@ -968,6 +968,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
localize(): void {
this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`);
this.category = i18next.t(`pokemonCategory:${SpeciesId[this.speciesId].toLowerCase()}_category`);
}
getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId {

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

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

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,
});
@ -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"),

View File

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

View File

@ -668,6 +668,9 @@ export class MovePhase extends BattlePhase {
}),
500,
);
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
// TODO: This assumes single target for message funcs - is this sustainable?
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
}

View File

@ -245,6 +245,7 @@ export async function initI18n(): Promise<void> {
"pokeball",
"pokedexUiHandler",
"pokemon",
"pokemonCategory",
"pokemonEvolutions",
"pokemonForm",
"pokemonInfo",
@ -298,6 +299,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

@ -174,6 +174,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container;
private pokemonCaughtCountText: Phaser.GameObjects.Text;
private pokemonFormText: Phaser.GameObjects.Text;
private pokemonCategoryText: Phaser.GameObjects.Text;
private pokemonHatchedIcon: Phaser.GameObjects.Sprite;
private pokemonHatchedCountText: Phaser.GameObjects.Text;
private pokemonShinyIcons: Phaser.GameObjects.Sprite[];
@ -409,6 +410,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonFormText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonFormText);
this.pokemonCategoryText = addTextObject(100, 18, "Category", TextStyle.WINDOW_ALT, {
fontSize: "42px",
});
this.pokemonCategoryText.setOrigin(1, 0);
this.starterSelectContainer.add(this.pokemonCategoryText);
this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25);
this.pokemonCaughtHatchedContainer.setScale(0.5);
this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer);
@ -2354,6 +2361,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
@ -2382,6 +2390,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
this.setSpeciesDetails(species!, {
// TODO: is this bang correct?
@ -2534,6 +2543,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonNameText.setText(species ? "???" : "");
}
// Setting the category
if (isFormCaught) {
this.pokemonCategoryText.setText(species.category);
} else {
this.pokemonCategoryText.setText("");
}
// Setting tint of the sprite
if (isFormCaught) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {

View File

@ -1,11 +1,14 @@
import { AbilityId } from "#enums/ability-id";
import { RandomMoveAttr } from "#app/data/moves/move";
import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { AbilityId } from "#app/enums/ability-id";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import i18next from "i18next";
import Phaser from "phaser";
//import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Chilly Reception", () => {
let phaserGame: Phaser.Game;
@ -25,95 +28,121 @@ describe("Moves - Chilly Reception", () => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE])
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE, MoveId.SPLASH, MoveId.METRONOME])
.enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH)
.ability(AbilityId.BALL_FETCH);
});
it("should still change the weather if user can't switch out", async () => {
it("should display message before use, switch the user out and change the weather to snow", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
it("should still change weather if user can't switch out", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
game.move.select(MoveId.CHILLY_RECEPTION);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
});
it("should switch out even if it's snowing", async () => {
it("should still switch out even if weather cannot be changed", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
// first turn set up snow with snowscape, try chilly reception on second turn
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.SNOWSCAPE);
await game.phaseInterceptor.to("BerryPhase", false);
await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
// TODO: Uncomment lines once wimp out PR fixes force switches to not reset summon data immediately
// await game.phaseInterceptor.to("SwitchSummonPhase", false);
// expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
});
it("happy case - switch out and weather changes", async () => {
// Source: https://replay.pokemonshowdown.com/gen9ou-2367532550
it("should fail (while still displaying message) if neither weather change nor switch out succeeds", async () => {
await game.classicMode.startBattle([SpeciesId.SLOWKING]);
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW);
const slowking = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SNOWSCAPE);
await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()).toBe(slowking);
expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(game.textInterceptor.logs).toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
it("should succeed without message if called indirectly", async () => {
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.CHILLY_RECEPTION);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1);
const [slowking, meowth] = game.scene.getPlayerParty();
game.move.select(MoveId.METRONOME);
game.doSelectPartyPokemon(1);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH);
expect(game.scene.getPlayerPokemon()).toBe(meowth);
expect(slowking.isOnField()).toBe(false);
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.textInterceptor.logs).not.toContain(
i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }),
);
});
// enemy uses another move and weather doesn't change
it("check case - enemy not selecting chilly reception doesn't change weather ", async () => {
game.override.battleStyle("single").enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]).moveset(MoveId.SPLASH);
// Bugcheck test for enemy AI bug
it("check case - enemy not selecting chilly reception doesn't change weather", async () => {
game.override.enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.SPLASH);
await game.move.selectEnemyMove(MoveId.TACKLE);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(undefined);
});
it("enemy trainer - expected behavior ", async () => {
game.override
.battleStyle("single")
.startingWave(8)
.enemyMoveset(MoveId.CHILLY_RECEPTION)
.enemySpecies(SpeciesId.MAGIKARP)
.moveset([MoveId.SPLASH, MoveId.THUNDERBOLT]);
await game.classicMode.startBattle([SpeciesId.JOLTEON]);
const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id;
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_MAGIKARP1);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.SPLASH);
// second chilly reception should still switch out
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
expect(game.scene.getEnemyPokemon()?.id === RIVAL_MAGIKARP1);
game.move.select(MoveId.THUNDERBOLT);
// enemy chilly recep move should fail: it's snowing and no option to switch out
// no crashing
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
expect(game.scene.arena.weather?.weatherType).toBeUndefined();
});
});

View File

@ -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, MysteryEncounterType[]>([
[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();
});
});
});

View File

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