mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 09:02:47 +02:00
Fix #5487: Implements Sky Battle Mystery Encounter
Adds a new Mystery Encounter, a Sky Battle, which is inspired by the same type of battle in the mainline games. This battle is triggered if the player has enough flying pokemon. Co-authored-by: José Marques <jose.serrado.marques@tecnico.ulisboa.pt>
This commit is contained in:
parent
7c6189e812
commit
821e380e57
41
public/images/trainer/sky_trainer_f.json
Normal file
41
public/images/trainer/sky_trainer_f.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "sky_trainer_f.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 56,
|
||||||
|
"h": 67
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "0001.png",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": true,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 56,
|
||||||
|
"h": 67
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 56,
|
||||||
|
"h": 67
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 56,
|
||||||
|
"h": 67
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:d051332055512f245ae68e912a28cd81:4926ba3fd9fe432bb535d13f4df1df4e:621aca0914900a3b9e235ebf0431ce2b$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/trainer/sky_trainer_f.png
Normal file
BIN
public/images/trainer/sky_trainer_f.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
41
public/images/trainer/sky_trainer_m.json
Normal file
41
public/images/trainer/sky_trainer_m.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "sky_trainer_m.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 48,
|
||||||
|
"h": 79
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "0001.png",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": true,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 48,
|
||||||
|
"h": 79
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 48,
|
||||||
|
"h": 79
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 48,
|
||||||
|
"h": 79
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:d051332055512f245ae68e912a28cd81:4926ba3fd9fe432bb535d13f4df1df4e:621aca0914900a3b9e235ebf0431ce2b$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/trainer/sky_trainer_m.png
Normal file
BIN
public/images/trainer/sky_trainer_m.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1006 B |
482
src/data/mystery-encounters/encounters/sky-battle-encounter.ts
Normal file
482
src/data/mystery-encounters/encounters/sky-battle-encounter.ts
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
|
||||||
|
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
|
||||||
|
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||||
|
import {
|
||||||
|
type EnemyPartyConfig,
|
||||||
|
initBattleWithEnemyConfig,
|
||||||
|
leaveEncounterWithoutBattle,
|
||||||
|
selectOptionThenPokemon,
|
||||||
|
setEncounterRewards,
|
||||||
|
transitionMysteryEncounterIntroVisuals,
|
||||||
|
} from "../utils/encounter-phase-utils";
|
||||||
|
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||||
|
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||||
|
import {
|
||||||
|
AbilityRequirement,
|
||||||
|
AnyCombinationPokemonRequirement,
|
||||||
|
TypeRequirement,
|
||||||
|
} from "../mystery-encounter-requirements";
|
||||||
|
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { randSeedInt, randSeedShuffle } from "#app/utils/common";
|
||||||
|
import { type PlayerPokemon, PokemonMove } from "#app/field/pokemon";
|
||||||
|
import i18next from "i18next";
|
||||||
|
import MoveInfoOverlay from "#app/ui/move-info-overlay";
|
||||||
|
import { showEncounterDialogue } from "../utils/encounter-dialogue-utils";
|
||||||
|
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
|
||||||
|
import { allMoves } from "#app/data/data-lists";
|
||||||
|
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||||
|
import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
|
||||||
|
import { getRandomPartyMemberFunc, type TrainerConfig, trainerConfigs } from "#app/data/trainers/trainer-config";
|
||||||
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||||
|
import { TrainerSlot } from "#enums/trainer-slot";
|
||||||
|
|
||||||
|
/** The i18n namespace for the encounter */
|
||||||
|
const namespace = "mysteryEncounters/skyBattle";
|
||||||
|
|
||||||
|
const SKY_BATTLE_WAVES: [number, number] = [50, 180];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These pokemon come from serebii's
|
||||||
|
* {@link https://www.serebii.net/xy/skybattles.shtml | Sky Battle Page}
|
||||||
|
*/
|
||||||
|
const POOL_0_POKEMON = [
|
||||||
|
SpeciesId.CHARIZARD,
|
||||||
|
SpeciesId.BUTTERFREE,
|
||||||
|
SpeciesId.PIDGEOTTO,
|
||||||
|
SpeciesId.PIDGEOT,
|
||||||
|
SpeciesId.FEAROW,
|
||||||
|
SpeciesId.ZUBAT,
|
||||||
|
SpeciesId.GOLBAT,
|
||||||
|
SpeciesId.HAUNTER,
|
||||||
|
SpeciesId.KOFFING,
|
||||||
|
SpeciesId.WEEZING,
|
||||||
|
SpeciesId.SCYTHER,
|
||||||
|
SpeciesId.GYARADOS,
|
||||||
|
SpeciesId.AERODACTYL,
|
||||||
|
SpeciesId.ARTICUNO,
|
||||||
|
SpeciesId.ZAPDOS,
|
||||||
|
SpeciesId.MOLTRES,
|
||||||
|
SpeciesId.DRAGONITE,
|
||||||
|
SpeciesId.NOCTOWL,
|
||||||
|
SpeciesId.LEDYBA,
|
||||||
|
SpeciesId.LEDIAN,
|
||||||
|
SpeciesId.CROBAT,
|
||||||
|
SpeciesId.TOGETIC,
|
||||||
|
SpeciesId.XATU,
|
||||||
|
SpeciesId.HOPPIP,
|
||||||
|
SpeciesId.SKIPLOOM,
|
||||||
|
SpeciesId.JUMPLUFF,
|
||||||
|
SpeciesId.YANMA,
|
||||||
|
SpeciesId.MISDREAVUS,
|
||||||
|
SpeciesId.UNOWN,
|
||||||
|
SpeciesId.GLIGAR,
|
||||||
|
SpeciesId.MANTINE,
|
||||||
|
SpeciesId.SKARMORY,
|
||||||
|
SpeciesId.LUGIA,
|
||||||
|
SpeciesId.HO_OH,
|
||||||
|
SpeciesId.BEAUTIFLY,
|
||||||
|
SpeciesId.SWELLOW,
|
||||||
|
SpeciesId.WINGULL,
|
||||||
|
SpeciesId.PELIPPER,
|
||||||
|
SpeciesId.MASQUERAIN,
|
||||||
|
SpeciesId.NINJASK,
|
||||||
|
SpeciesId.VIBRAVA,
|
||||||
|
SpeciesId.FLYGON,
|
||||||
|
SpeciesId.SWABLU,
|
||||||
|
SpeciesId.ALTARIA,
|
||||||
|
SpeciesId.LUNATONE,
|
||||||
|
SpeciesId.SOLROCK,
|
||||||
|
SpeciesId.BALTOY,
|
||||||
|
SpeciesId.CLAYDOL,
|
||||||
|
SpeciesId.DUSKULL,
|
||||||
|
SpeciesId.TROPIUS,
|
||||||
|
SpeciesId.CHIMECHO,
|
||||||
|
SpeciesId.SALAMENCE,
|
||||||
|
SpeciesId.LATIAS,
|
||||||
|
SpeciesId.LATIOS,
|
||||||
|
SpeciesId.RAYQUAZA,
|
||||||
|
SpeciesId.STARAVIA,
|
||||||
|
SpeciesId.STARAPTOR,
|
||||||
|
SpeciesId.MOTHIM,
|
||||||
|
SpeciesId.COMBEE,
|
||||||
|
SpeciesId.VESPIQUEN,
|
||||||
|
SpeciesId.DRIFLOON,
|
||||||
|
SpeciesId.DRIFBLIM,
|
||||||
|
SpeciesId.MISMAGIUS,
|
||||||
|
SpeciesId.HONCHKROW,
|
||||||
|
SpeciesId.CHINGLING,
|
||||||
|
SpeciesId.BRONZOR,
|
||||||
|
SpeciesId.BRONZONG,
|
||||||
|
SpeciesId.CARNIVINE,
|
||||||
|
SpeciesId.MANTYKE,
|
||||||
|
SpeciesId.TOGEKISS,
|
||||||
|
SpeciesId.YANMEGA,
|
||||||
|
SpeciesId.GLISCOR,
|
||||||
|
SpeciesId.ROTOM,
|
||||||
|
SpeciesId.UXIE,
|
||||||
|
SpeciesId.MESPRIT,
|
||||||
|
SpeciesId.AZELF,
|
||||||
|
SpeciesId.CRESSELIA,
|
||||||
|
SpeciesId.TRANQUILL,
|
||||||
|
SpeciesId.UNFEZANT,
|
||||||
|
SpeciesId.WOOBAT,
|
||||||
|
SpeciesId.SWOOBAT,
|
||||||
|
SpeciesId.SIGILYPH,
|
||||||
|
SpeciesId.ARCHEOPS,
|
||||||
|
SpeciesId.SWANNA,
|
||||||
|
SpeciesId.EMOLGA,
|
||||||
|
SpeciesId.TYNAMO,
|
||||||
|
SpeciesId.EELEKTRIK,
|
||||||
|
SpeciesId.EELEKTROSS,
|
||||||
|
SpeciesId.CRYOGONAL,
|
||||||
|
SpeciesId.BRAVIARY,
|
||||||
|
SpeciesId.MANDIBUZZ,
|
||||||
|
SpeciesId.HYDREIGON,
|
||||||
|
SpeciesId.TORNADUS,
|
||||||
|
SpeciesId.THUNDURUS,
|
||||||
|
SpeciesId.LANDORUS,
|
||||||
|
SpeciesId.FLETCHINDER,
|
||||||
|
SpeciesId.TALONFLAME,
|
||||||
|
SpeciesId.VIVILLON,
|
||||||
|
SpeciesId.NOIBAT,
|
||||||
|
SpeciesId.NOIVERN,
|
||||||
|
SpeciesId.YVELTAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
const PHYSICAL_TUTOR_MOVES = [
|
||||||
|
MoveId.FLY,
|
||||||
|
MoveId.BRAVE_BIRD,
|
||||||
|
MoveId.ACROBATICS,
|
||||||
|
MoveId.DRAGON_ASCENT,
|
||||||
|
MoveId.BEAK_BLAST,
|
||||||
|
MoveId.FLOATY_FALL,
|
||||||
|
MoveId.DUAL_WINGBEAT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SPECIAL_TUTOR_MOVES = [MoveId.AEROBLAST, MoveId.AIR_SLASH, MoveId.HURRICANE, MoveId.BLEAKWIND_STORM];
|
||||||
|
|
||||||
|
const SUPPORT_TUTOR_MOVES = [MoveId.FEATHER_DANCE, MoveId.ROOST, MoveId.PLUCK, MoveId.TAILWIND];
|
||||||
|
|
||||||
|
// Not sure the best way to do this
|
||||||
|
const INELIGIBLE_MOVES: MoveId[] = [
|
||||||
|
MoveId.BODY_SLAM,
|
||||||
|
MoveId.BULLDOZE,
|
||||||
|
MoveId.DIG,
|
||||||
|
MoveId.DIVE,
|
||||||
|
MoveId.EARTH_POWER,
|
||||||
|
MoveId.EARTHQUAKE,
|
||||||
|
MoveId.ELECTRIC_TERRAIN,
|
||||||
|
MoveId.FIRE_PLEDGE,
|
||||||
|
MoveId.FISSURE,
|
||||||
|
MoveId.FLYING_PRESS,
|
||||||
|
MoveId.FRENZY_PLANT,
|
||||||
|
MoveId.GEOMANCY,
|
||||||
|
MoveId.GRASS_KNOT,
|
||||||
|
MoveId.GRASS_PLEDGE,
|
||||||
|
MoveId.GRASSY_TERRAIN,
|
||||||
|
MoveId.GRAVITY,
|
||||||
|
MoveId.HEAVY_SLAM,
|
||||||
|
MoveId.INGRAIN,
|
||||||
|
MoveId.LANDS_WRATH,
|
||||||
|
MoveId.MAGNITUDE,
|
||||||
|
MoveId.MAT_BLOCK,
|
||||||
|
MoveId.MISTY_TERRAIN,
|
||||||
|
MoveId.MUD_SPORT,
|
||||||
|
MoveId.MUDDY_WATER,
|
||||||
|
MoveId.ROTOTILLER,
|
||||||
|
MoveId.SEISMIC_TOSS,
|
||||||
|
MoveId.SLAM,
|
||||||
|
MoveId.SMACK_DOWN,
|
||||||
|
MoveId.SPIKES,
|
||||||
|
MoveId.STOMP,
|
||||||
|
MoveId.SUBSTITUTE,
|
||||||
|
MoveId.SURF,
|
||||||
|
MoveId.TOXIC_SPIKES,
|
||||||
|
MoveId.WATER_PLEDGE,
|
||||||
|
MoveId.WATER_SPORT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const sky_battle_requirements = new AnyCombinationPokemonRequirement(
|
||||||
|
3,
|
||||||
|
new TypeRequirement(PokemonType.FLYING, false, 1),
|
||||||
|
new AbilityRequirement(AbilityId.LEVITATE, false, 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helpful variables
|
||||||
|
let originalUsedPP: number[] = [];
|
||||||
|
const disallowedPokemon: Map<number, PlayerPokemon> = new Map<number, PlayerPokemon>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sky Battle encounter.
|
||||||
|
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/5487 | GitHub Issue #5487}
|
||||||
|
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
|
||||||
|
*/
|
||||||
|
export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(
|
||||||
|
MysteryEncounterType.SKY_BATTLE,
|
||||||
|
)
|
||||||
|
.withPrimaryPokemonRequirement(sky_battle_requirements)
|
||||||
|
.withMaxAllowedEncounters(1)
|
||||||
|
.withEncounterTier(MysteryEncounterTier.ULTRA)
|
||||||
|
.withSceneWaveRangeRequirement(...SKY_BATTLE_WAVES)
|
||||||
|
.withIntroSpriteConfigs([]) // Sprite is set in onInit()
|
||||||
|
.withIntroDialogue([
|
||||||
|
{
|
||||||
|
text: `${namespace}:intro`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
speaker: `${namespace}:speaker`,
|
||||||
|
text: `${namespace}:intro_dialogue`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.withOnInit(() => {
|
||||||
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||||
|
const partySize: number = sky_battle_requirements.queryParty(globalScene.getPlayerParty()).length;
|
||||||
|
|
||||||
|
// randomize trainer gender
|
||||||
|
const female = !!randSeedInt(2);
|
||||||
|
const config = getTrainerConfig(partySize, female);
|
||||||
|
const spriteKey = config.getSpriteKey(female);
|
||||||
|
encounter.enemyPartyConfigs.push({
|
||||||
|
trainerConfig: config,
|
||||||
|
female: female,
|
||||||
|
});
|
||||||
|
|
||||||
|
// loads trainer sprite at start of encounter
|
||||||
|
encounter.spriteConfigs = [
|
||||||
|
{
|
||||||
|
spriteKey: spriteKey,
|
||||||
|
fileRoot: "trainer",
|
||||||
|
hasShadow: true,
|
||||||
|
x: 4,
|
||||||
|
y: 7,
|
||||||
|
yShadow: 7,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.setLocalizationKey(`${namespace}`)
|
||||||
|
.withTitle(`${namespace}:title`)
|
||||||
|
.withDescription(`${namespace}:description`)
|
||||||
|
.withQuery(`${namespace}:query`)
|
||||||
|
.withSimpleOption(
|
||||||
|
//Option 1: Battle
|
||||||
|
{
|
||||||
|
buttonLabel: `${namespace}:option.1.label`,
|
||||||
|
buttonTooltip: `${namespace}:option.1.tooltip`,
|
||||||
|
selected: [
|
||||||
|
{
|
||||||
|
speaker: `${namespace}:speaker`,
|
||||||
|
text: `${namespace}:option.1.selected`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
// Select sky battle
|
||||||
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||||
|
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
|
||||||
|
|
||||||
|
// Init the moves available for tutor
|
||||||
|
const moveTutorOptions: PokemonMove[] = [];
|
||||||
|
moveTutorOptions.push(new PokemonMove(PHYSICAL_TUTOR_MOVES[randSeedInt(PHYSICAL_TUTOR_MOVES.length)]));
|
||||||
|
moveTutorOptions.push(new PokemonMove(SPECIAL_TUTOR_MOVES[randSeedInt(SPECIAL_TUTOR_MOVES.length)]));
|
||||||
|
moveTutorOptions.push(new PokemonMove(SUPPORT_TUTOR_MOVES[randSeedInt(SUPPORT_TUTOR_MOVES.length)]));
|
||||||
|
encounter.misc = {
|
||||||
|
moveTutorOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
//Remove disallowed pokemon
|
||||||
|
const allowedPokemon = sky_battle_requirements.queryParty(globalScene.getPlayerParty());
|
||||||
|
globalScene.getPlayerParty().filter(pokemon => !allowedPokemon.includes(pokemon));
|
||||||
|
globalScene.getPlayerParty().map((pokemon, index) => {
|
||||||
|
if (!allowedPokemon.includes(pokemon)) {
|
||||||
|
disallowedPokemon.set(index, pokemon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
disallowedPokemon.forEach(pokemon => globalScene.removePokemonFromPlayerParty(pokemon, false));
|
||||||
|
|
||||||
|
//Set illegal pokemon moves pp to 0
|
||||||
|
originalUsedPP = [];
|
||||||
|
globalScene.getPlayerParty().forEach(pokemon =>
|
||||||
|
pokemon.moveset
|
||||||
|
.filter(move => INELIGIBLE_MOVES.includes(move.getMove().id))
|
||||||
|
.forEach(move => {
|
||||||
|
originalUsedPP.push(move.ppUsed);
|
||||||
|
move.ppUsed = move.getMovePp();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assigns callback that teaches move before continuing to rewards
|
||||||
|
encounter.onRewards = doFlyingTypeTutor;
|
||||||
|
|
||||||
|
setEncounterRewards({ fillRemaining: true });
|
||||||
|
await transitionMysteryEncounterIntroVisuals(true, true);
|
||||||
|
await initBattleWithEnemyConfig(config);
|
||||||
|
|
||||||
|
//Set illegal enemy pokemon moves pp to 0
|
||||||
|
globalScene.getEnemyParty().forEach(pokemon =>
|
||||||
|
pokemon.moveset
|
||||||
|
.filter(move => INELIGIBLE_MOVES.includes(move.getMove().id))
|
||||||
|
.forEach(move => {
|
||||||
|
move.ppUsed = move.getMovePp();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.withOption(
|
||||||
|
//Option 2: Flaunt flying pokemon
|
||||||
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||||
|
.withPrimaryPokemonRequirement(sky_battle_requirements) // Must pass the same requirements to trigger this encounter
|
||||||
|
.withDialogue({
|
||||||
|
buttonLabel: `${namespace}:option.2.label`,
|
||||||
|
buttonTooltip: `${namespace}:option.2.tooltip`,
|
||||||
|
disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`,
|
||||||
|
})
|
||||||
|
.withPreOptionPhase(async () => {
|
||||||
|
// Player shows off their Flying pokemon
|
||||||
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||||
|
|
||||||
|
setEncounterRewards({
|
||||||
|
guaranteedModifierTypeFuncs: [modifierTypes.QUICK_CLAW, modifierTypes.MAX_LURE, modifierTypes.ULTRA_BALL],
|
||||||
|
fillRemaining: false,
|
||||||
|
});
|
||||||
|
encounter.selectedOption!.dialogue!.selected = [
|
||||||
|
{
|
||||||
|
speaker: `${namespace}:speaker`,
|
||||||
|
text: `${namespace}:option.2.selected`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.withOptionPhase(async () => {
|
||||||
|
// Player shows off their Flying pokémon
|
||||||
|
leaveEncounterWithoutBattle();
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.withSimpleOption(
|
||||||
|
//Option 3: Reject battle and leave with no rewards
|
||||||
|
{
|
||||||
|
buttonLabel: `${namespace}:option.3.label`,
|
||||||
|
buttonTooltip: `${namespace}:option.3.tooltip`,
|
||||||
|
selected: [
|
||||||
|
{
|
||||||
|
text: `${namespace}:option.3.selected`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
leaveEncounterWithoutBattle();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.withOutroDialogue([
|
||||||
|
{
|
||||||
|
text: `${namespace}:outro`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
function getTrainerConfig(party_size: number, female: boolean): TrainerConfig {
|
||||||
|
// Sky trainer config
|
||||||
|
const config = trainerConfigs[TrainerType.SKY_TRAINER].clone();
|
||||||
|
const name = female ? "sky_trainer_f" : "sky_trainer_m";
|
||||||
|
config.name = i18next.t("trainerNames:" + name);
|
||||||
|
|
||||||
|
let pool0Copy = POOL_0_POKEMON.slice(0);
|
||||||
|
pool0Copy = randSeedShuffle(pool0Copy);
|
||||||
|
let pool0Mon = pool0Copy.pop()!;
|
||||||
|
|
||||||
|
config.setPartyTemplates(new TrainerPartyTemplate(party_size, PartyMemberStrength.STRONG));
|
||||||
|
|
||||||
|
// adds a non-repeating random pokemon
|
||||||
|
for (let index = 0; index < party_size; index++) {
|
||||||
|
config.setPartyMemberFunc(index, getRandomPartyMemberFunc([pool0Mon], TrainerSlot.TRAINER, true));
|
||||||
|
pool0Mon = pool0Copy.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doFlyingTypeTutor(): Promise<void> {
|
||||||
|
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO explain
|
||||||
|
return new Promise<void>(async resolve => {
|
||||||
|
const moveOptions = globalScene.currentBattle.mysteryEncounter!.misc.moveTutorOptions;
|
||||||
|
await showEncounterDialogue(`${namespace}:battle_won`, `${namespace}:speaker`);
|
||||||
|
|
||||||
|
const overlayScale = 1;
|
||||||
|
const moveInfoOverlay = new MoveInfoOverlay({
|
||||||
|
delayVisibility: false,
|
||||||
|
scale: overlayScale,
|
||||||
|
onSide: true,
|
||||||
|
right: true,
|
||||||
|
x: 1,
|
||||||
|
y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1,
|
||||||
|
width: globalScene.game.canvas.width / 6 - 2,
|
||||||
|
});
|
||||||
|
globalScene.ui.add(moveInfoOverlay);
|
||||||
|
|
||||||
|
const optionSelectItems = moveOptions.map((move: PokemonMove) => {
|
||||||
|
const option: OptionSelectItem = {
|
||||||
|
label: move.getName(),
|
||||||
|
handler: () => {
|
||||||
|
moveInfoOverlay.active = false;
|
||||||
|
moveInfoOverlay.setVisible(false);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onHover: () => {
|
||||||
|
moveInfoOverlay.active = true;
|
||||||
|
moveInfoOverlay.show(allMoves[move.moveId]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onHoverOverCancel = () => {
|
||||||
|
moveInfoOverlay.active = false;
|
||||||
|
moveInfoOverlay.setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await selectOptionThenPokemon(
|
||||||
|
optionSelectItems,
|
||||||
|
`${namespace}:teach_move_prompt`,
|
||||||
|
undefined, // No filter
|
||||||
|
onHoverOverCancel,
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
moveInfoOverlay.active = false;
|
||||||
|
moveInfoOverlay.setVisible(false);
|
||||||
|
}
|
||||||
|
// Option select complete, handle if they are learning a move
|
||||||
|
if (result && result.selectedOptionIndex < moveOptions.length) {
|
||||||
|
globalScene.unshiftPhase(
|
||||||
|
new LearnMovePhase(result.selectedPokemonIndex, moveOptions[result.selectedOptionIndex].moveId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset ineligible moves' pp
|
||||||
|
let idx = 0;
|
||||||
|
globalScene.getPlayerParty().forEach(pokemon =>
|
||||||
|
pokemon.moveset
|
||||||
|
.filter(move => INELIGIBLE_MOVES.includes(move.getMove().id))
|
||||||
|
.forEach(move => {
|
||||||
|
move.ppUsed = originalUsedPP[idx++];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
//Return disallowed pokemons
|
||||||
|
disallowedPokemon.forEach((pokemon, index) => {
|
||||||
|
globalScene.getPlayerParty().splice(index, 0, pokemon);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete battle and go to rewards
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
@ -1213,3 +1213,51 @@ export class WeightRequirement extends EncounterPokemonRequirement {
|
|||||||
return ["weight", pokemon?.getWeight().toString() ?? ""];
|
return ["weight", pokemon?.getWeight().toString() ?? ""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class tests for the minimum number of pokemon that passes any of the requirements
|
||||||
|
* E.g. having 3 pokemon that are either flying type, or have the levitate ability
|
||||||
|
*/
|
||||||
|
export class AnyCombinationPokemonRequirement extends EncounterPokemonRequirement {
|
||||||
|
private requirements: EncounterPokemonRequirement[];
|
||||||
|
|
||||||
|
constructor(minNumberOfPokemon: number, ...requirements: EncounterPokemonRequirement[]) {
|
||||||
|
super();
|
||||||
|
this.invertQuery = false;
|
||||||
|
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||||
|
this.requirements = requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if at least {@linkcode minNumberOfPokemon} pokemon meet any of the requirements
|
||||||
|
* @returns true if at least {@linkcode minNumberOfPokemon} pokemon meet any of the requirements
|
||||||
|
*/
|
||||||
|
override meetsRequirement(): boolean {
|
||||||
|
const party = globalScene.getPlayerParty();
|
||||||
|
return this.queryParty(party).length >= this.minNumberOfPokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the players party for all party members that are compatible with any of the requirements
|
||||||
|
* @param partyPokemon The party of {@linkcode PlayerPokemon}
|
||||||
|
* @returns All party members that are compatible with any of the requirements
|
||||||
|
*/
|
||||||
|
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||||
|
return partyPokemon.filter(pokemon => this.requirements.some(req => req.queryParty([pokemon]).length !== 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a dialogue token key/value pair for the given {@linkcode EncounterPokemonRequirement | requirements}.
|
||||||
|
* @param pokemon The {@linkcode PlayerPokemon} to check against
|
||||||
|
* @returns A dialogue token key/value pair
|
||||||
|
*/
|
||||||
|
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
|
||||||
|
for (const req of this.requirements) {
|
||||||
|
if (req.meetsRequirement()) {
|
||||||
|
return req.getDialogueToken(pokemon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.requirements[0].getDialogueToken(pokemon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,6 +33,7 @@ import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/
|
|||||||
import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-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 { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter";
|
||||||
import { getBiomeName } from "#app/data/balance/biomes";
|
import { getBiomeName } from "#app/data/balance/biomes";
|
||||||
|
import { SkyBattleEncounter } from "./encounters/sky-battle-encounter";
|
||||||
|
|
||||||
export const EXTREME_ENCOUNTER_BIOMES = [
|
export const EXTREME_ENCOUNTER_BIOMES = [
|
||||||
BiomeId.SEA,
|
BiomeId.SEA,
|
||||||
@ -135,6 +136,36 @@ export const CIVILIZATION_ENCOUNTER_BIOMES = [
|
|||||||
BiomeId.ISLAND,
|
BiomeId.ISLAND,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places where you could fly like a bird
|
||||||
|
*/
|
||||||
|
export const OPEN_SKY_BIOMES = [
|
||||||
|
BiomeId.TOWN,
|
||||||
|
BiomeId.PLAINS,
|
||||||
|
BiomeId.GRASS,
|
||||||
|
BiomeId.TALL_GRASS,
|
||||||
|
BiomeId.METROPOLIS,
|
||||||
|
BiomeId.FOREST,
|
||||||
|
BiomeId.SEA,
|
||||||
|
BiomeId.SWAMP,
|
||||||
|
BiomeId.BEACH,
|
||||||
|
BiomeId.LAKE,
|
||||||
|
BiomeId.MOUNTAIN,
|
||||||
|
BiomeId.BADLANDS,
|
||||||
|
BiomeId.DESERT,
|
||||||
|
BiomeId.MEADOW,
|
||||||
|
BiomeId.VOLCANO,
|
||||||
|
BiomeId.GRAVEYARD,
|
||||||
|
BiomeId.RUINS,
|
||||||
|
BiomeId.WASTELAND,
|
||||||
|
BiomeId.CONSTRUCTION_SITE,
|
||||||
|
BiomeId.JUNGLE,
|
||||||
|
BiomeId.TEMPLE,
|
||||||
|
BiomeId.SLUM,
|
||||||
|
BiomeId.SNOWY_FOREST,
|
||||||
|
BiomeId.ISLAND,
|
||||||
|
];
|
||||||
|
|
||||||
export const allMysteryEncounters: {
|
export const allMysteryEncounters: {
|
||||||
[encounterType: number]: MysteryEncounter;
|
[encounterType: number]: MysteryEncounter;
|
||||||
} = {};
|
} = {};
|
||||||
@ -162,6 +193,8 @@ const civilizationBiomeEncounters: MysteryEncounterType[] = [
|
|||||||
MysteryEncounterType.GLOBAL_TRADE_SYSTEM,
|
MysteryEncounterType.GLOBAL_TRADE_SYSTEM,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const openSkyBiomeEncounters: MysteryEncounterType[] = [MysteryEncounterType.SKY_BATTLE];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To add an encounter to every biome possible, use this array
|
* To add an encounter to every biome possible, use this array
|
||||||
*/
|
*/
|
||||||
@ -257,6 +290,7 @@ export function initMysteryEncounters() {
|
|||||||
allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter;
|
allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter;
|
||||||
allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter;
|
allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter;
|
||||||
allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter;
|
allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter;
|
||||||
|
allMysteryEncounters[MysteryEncounterType.SKY_BATTLE] = SkyBattleEncounter;
|
||||||
|
|
||||||
// Add extreme encounters to biome map
|
// Add extreme encounters to biome map
|
||||||
extremeBiomeEncounters.forEach(encounter => {
|
extremeBiomeEncounters.forEach(encounter => {
|
||||||
@ -295,6 +329,16 @@ export function initMysteryEncounters() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// add open sky encounters to biome map
|
||||||
|
openSkyBiomeEncounters.forEach(encounter => {
|
||||||
|
OPEN_SKY_BIOMES.forEach(biome => {
|
||||||
|
const encountersForBiome = mysteryEncountersByBiome.get(biome);
|
||||||
|
if (encountersForBiome && !encountersForBiome.includes(encounter)) {
|
||||||
|
encountersForBiome.push(encounter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Add ANY biome encounters to biome map
|
// Add ANY biome encounters to biome map
|
||||||
let _encounterBiomeTableLog = "";
|
let _encounterBiomeTableLog = "";
|
||||||
mysteryEncountersByBiome.forEach((biomeEncounters, biome) => {
|
mysteryEncountersByBiome.forEach((biomeEncounters, biome) => {
|
||||||
|
@ -6045,4 +6045,9 @@ export const trainerConfigs: TrainerConfigs = {
|
|||||||
.setVictoryBgm("mystery_encounter_weird_dream")
|
.setVictoryBgm("mystery_encounter_weird_dream")
|
||||||
.setLocalizedName("Future Self F")
|
.setLocalizedName("Future Self F")
|
||||||
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)),
|
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)),
|
||||||
|
[TrainerType.SKY_TRAINER]: new TrainerConfig(++t)
|
||||||
|
.setHasGenders("Sky Trainer Felicia")
|
||||||
|
.setMoneyMultiplier(2.25)
|
||||||
|
.setEncounterBgm(TrainerType.ACE_TRAINER)
|
||||||
|
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)),
|
||||||
};
|
};
|
||||||
|
@ -29,5 +29,6 @@ export enum MysteryEncounterType {
|
|||||||
FUN_AND_GAMES,
|
FUN_AND_GAMES,
|
||||||
UNCOMMON_BREED,
|
UNCOMMON_BREED,
|
||||||
GLOBAL_TRADE_SYSTEM,
|
GLOBAL_TRADE_SYSTEM,
|
||||||
THE_EXPERT_POKEMON_BREEDER
|
THE_EXPERT_POKEMON_BREEDER,
|
||||||
|
SKY_BATTLE,
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,7 @@ export enum TrainerType {
|
|||||||
EXPERT_POKEMON_BREEDER,
|
EXPERT_POKEMON_BREEDER,
|
||||||
FUTURE_SELF_M,
|
FUTURE_SELF_M,
|
||||||
FUTURE_SELF_F,
|
FUTURE_SELF_F,
|
||||||
|
SKY_TRAINER,
|
||||||
|
|
||||||
BROCK = 200,
|
BROCK = 200,
|
||||||
MISTY,
|
MISTY,
|
||||||
|
@ -174,7 +174,24 @@ export async function initI18n(): Promise<void> {
|
|||||||
"es-MX": ["es-ES", "en"],
|
"es-MX": ["es-ES", "en"],
|
||||||
default: ["en"],
|
default: ["en"],
|
||||||
},
|
},
|
||||||
supportedLngs: ["en", "es-ES", "es-MX", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca", "da", "tr", "ro", "ru"],
|
supportedLngs: [
|
||||||
|
"en",
|
||||||
|
"es-ES",
|
||||||
|
"es-MX",
|
||||||
|
"fr",
|
||||||
|
"it",
|
||||||
|
"de",
|
||||||
|
"zh-CN",
|
||||||
|
"zh-TW",
|
||||||
|
"pt-BR",
|
||||||
|
"ko",
|
||||||
|
"ja",
|
||||||
|
"ca",
|
||||||
|
"da",
|
||||||
|
"tr",
|
||||||
|
"ro",
|
||||||
|
"ru",
|
||||||
|
],
|
||||||
backend: {
|
backend: {
|
||||||
loadPath(lng: string, [ns]: string[]) {
|
loadPath(lng: string, [ns]: string[]) {
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
@ -276,6 +293,7 @@ export async function initI18n(): Promise<void> {
|
|||||||
"mysteryEncounters/theWinstrateChallenge",
|
"mysteryEncounters/theWinstrateChallenge",
|
||||||
"mysteryEncounters/teleportingHijinks",
|
"mysteryEncounters/teleportingHijinks",
|
||||||
"mysteryEncounters/bugTypeSuperfan",
|
"mysteryEncounters/bugTypeSuperfan",
|
||||||
|
"mysteryEncounters/skyBattle",
|
||||||
"mysteryEncounters/funAndGames",
|
"mysteryEncounters/funAndGames",
|
||||||
"mysteryEncounters/uncommonBreed",
|
"mysteryEncounters/uncommonBreed",
|
||||||
"mysteryEncounters/globalTradeSystem",
|
"mysteryEncounters/globalTradeSystem",
|
||||||
|
408
test/mystery-encounter/encounters/sky-battle-encounter.test.ts
Normal file
408
test/mystery-encounter/encounters/sky-battle-encounter.test.ts
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
|
||||||
|
import { BiomeId } from "#app/enums/biome-id";
|
||||||
|
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
|
||||||
|
import { SpeciesId } from "#app/enums/species-id";
|
||||||
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
runMysteryEncounterToEnd,
|
||||||
|
runSelectMysteryEncounterOption,
|
||||||
|
skipBattleRunMysteryEncounterRewardsPhase,
|
||||||
|
} from "#test/mystery-encounter/encounter-test-utils";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import type BattleScene from "#app/battle-scene";
|
||||||
|
import { PokemonMove } from "#app/field/pokemon";
|
||||||
|
import { UiMode } from "#enums/ui-mode";
|
||||||
|
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||||
|
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||||
|
import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils";
|
||||||
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases";
|
||||||
|
import { CommandPhase } from "#app/phases/command-phase";
|
||||||
|
import * as encounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||||
|
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||||
|
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
||||||
|
import { SkyBattleEncounter } from "#app/data/mystery-encounters/encounters/sky-battle-encounter";
|
||||||
|
import { Button } from "#enums/buttons";
|
||||||
|
|
||||||
|
const namespace = "mysteryEncounters/skyBattle";
|
||||||
|
const defaultParty = [SpeciesId.RAYQUAZA, SpeciesId.WEEDLE, SpeciesId.FLYGON, SpeciesId.RATTATA, SpeciesId.AERODACTYL];
|
||||||
|
const defaultBiome = BiomeId.BEACH;
|
||||||
|
const defaultWave = 52;
|
||||||
|
|
||||||
|
const POOL_0_POKEMON = [
|
||||||
|
SpeciesId.CHARIZARD,
|
||||||
|
SpeciesId.BUTTERFREE,
|
||||||
|
SpeciesId.PIDGEOTTO,
|
||||||
|
SpeciesId.PIDGEOT,
|
||||||
|
SpeciesId.FEAROW,
|
||||||
|
SpeciesId.ZUBAT,
|
||||||
|
SpeciesId.GOLBAT,
|
||||||
|
SpeciesId.HAUNTER,
|
||||||
|
SpeciesId.KOFFING,
|
||||||
|
SpeciesId.WEEZING,
|
||||||
|
SpeciesId.SCYTHER,
|
||||||
|
SpeciesId.GYARADOS,
|
||||||
|
SpeciesId.AERODACTYL,
|
||||||
|
SpeciesId.ARTICUNO,
|
||||||
|
SpeciesId.ZAPDOS,
|
||||||
|
SpeciesId.MOLTRES,
|
||||||
|
SpeciesId.DRAGONITE,
|
||||||
|
SpeciesId.NOCTOWL,
|
||||||
|
SpeciesId.LEDYBA,
|
||||||
|
SpeciesId.LEDIAN,
|
||||||
|
SpeciesId.CROBAT,
|
||||||
|
SpeciesId.TOGETIC,
|
||||||
|
SpeciesId.XATU,
|
||||||
|
SpeciesId.HOPPIP,
|
||||||
|
SpeciesId.SKIPLOOM,
|
||||||
|
SpeciesId.JUMPLUFF,
|
||||||
|
SpeciesId.YANMA,
|
||||||
|
SpeciesId.MISDREAVUS,
|
||||||
|
SpeciesId.UNOWN,
|
||||||
|
SpeciesId.GLIGAR,
|
||||||
|
SpeciesId.MANTINE,
|
||||||
|
SpeciesId.SKARMORY,
|
||||||
|
SpeciesId.LUGIA,
|
||||||
|
SpeciesId.HO_OH,
|
||||||
|
SpeciesId.BEAUTIFLY,
|
||||||
|
SpeciesId.SWELLOW,
|
||||||
|
SpeciesId.WINGULL,
|
||||||
|
SpeciesId.PELIPPER,
|
||||||
|
SpeciesId.MASQUERAIN,
|
||||||
|
SpeciesId.NINJASK,
|
||||||
|
SpeciesId.VIBRAVA,
|
||||||
|
SpeciesId.FLYGON,
|
||||||
|
SpeciesId.SWABLU,
|
||||||
|
SpeciesId.ALTARIA,
|
||||||
|
SpeciesId.LUNATONE,
|
||||||
|
SpeciesId.SOLROCK,
|
||||||
|
SpeciesId.BALTOY,
|
||||||
|
SpeciesId.CLAYDOL,
|
||||||
|
SpeciesId.DUSKULL,
|
||||||
|
SpeciesId.TROPIUS,
|
||||||
|
SpeciesId.CHIMECHO,
|
||||||
|
SpeciesId.SALAMENCE,
|
||||||
|
SpeciesId.LATIAS,
|
||||||
|
SpeciesId.LATIOS,
|
||||||
|
SpeciesId.RAYQUAZA,
|
||||||
|
SpeciesId.STARAVIA,
|
||||||
|
SpeciesId.STARAPTOR,
|
||||||
|
SpeciesId.MOTHIM,
|
||||||
|
SpeciesId.COMBEE,
|
||||||
|
SpeciesId.VESPIQUEN,
|
||||||
|
SpeciesId.DRIFLOON,
|
||||||
|
SpeciesId.DRIFBLIM,
|
||||||
|
SpeciesId.MISMAGIUS,
|
||||||
|
SpeciesId.HONCHKROW,
|
||||||
|
SpeciesId.CHINGLING,
|
||||||
|
SpeciesId.BRONZOR,
|
||||||
|
SpeciesId.BRONZONG,
|
||||||
|
SpeciesId.CARNIVINE,
|
||||||
|
SpeciesId.MANTYKE,
|
||||||
|
SpeciesId.TOGEKISS,
|
||||||
|
SpeciesId.YANMEGA,
|
||||||
|
SpeciesId.GLISCOR,
|
||||||
|
SpeciesId.ROTOM,
|
||||||
|
SpeciesId.UXIE,
|
||||||
|
SpeciesId.MESPRIT,
|
||||||
|
SpeciesId.AZELF,
|
||||||
|
SpeciesId.CRESSELIA,
|
||||||
|
SpeciesId.TRANQUILL,
|
||||||
|
SpeciesId.UNFEZANT,
|
||||||
|
SpeciesId.WOOBAT,
|
||||||
|
SpeciesId.SWOOBAT,
|
||||||
|
SpeciesId.SIGILYPH,
|
||||||
|
SpeciesId.ARCHEOPS,
|
||||||
|
SpeciesId.SWANNA,
|
||||||
|
SpeciesId.EMOLGA,
|
||||||
|
SpeciesId.TYNAMO,
|
||||||
|
SpeciesId.EELEKTRIK,
|
||||||
|
SpeciesId.EELEKTROSS,
|
||||||
|
SpeciesId.CRYOGONAL,
|
||||||
|
SpeciesId.BRAVIARY,
|
||||||
|
SpeciesId.MANDIBUZZ,
|
||||||
|
SpeciesId.HYDREIGON,
|
||||||
|
SpeciesId.TORNADUS,
|
||||||
|
SpeciesId.THUNDURUS,
|
||||||
|
SpeciesId.LANDORUS,
|
||||||
|
SpeciesId.FLETCHINDER,
|
||||||
|
SpeciesId.TALONFLAME,
|
||||||
|
SpeciesId.VIVILLON,
|
||||||
|
SpeciesId.NOIBAT,
|
||||||
|
SpeciesId.NOIVERN,
|
||||||
|
SpeciesId.YVELTAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
const PHYSICAL_TUTOR_MOVES = [
|
||||||
|
MoveId.FLY,
|
||||||
|
MoveId.BRAVE_BIRD,
|
||||||
|
MoveId.ACROBATICS,
|
||||||
|
MoveId.DRAGON_ASCENT,
|
||||||
|
MoveId.BEAK_BLAST,
|
||||||
|
MoveId.FLOATY_FALL,
|
||||||
|
MoveId.DUAL_WINGBEAT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SPECIAL_TUTOR_MOVES = [MoveId.AEROBLAST, MoveId.AIR_SLASH, MoveId.HURRICANE, MoveId.BLEAKWIND_STORM];
|
||||||
|
|
||||||
|
const SUPPORT_TUTOR_MOVES = [MoveId.FEATHER_DANCE, MoveId.ROOST, MoveId.PLUCK, MoveId.TAILWIND];
|
||||||
|
|
||||||
|
describe("Sky Battle - 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();
|
||||||
|
|
||||||
|
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
|
||||||
|
new Map<BiomeId, MysteryEncounterType[]>([[BiomeId.BEACH, [MysteryEncounterType.SKY_BATTLE]]]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the correct properties", async () => {
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
|
||||||
|
expect(SkyBattleEncounter.encounterType).toBe(MysteryEncounterType.SKY_BATTLE);
|
||||||
|
expect(SkyBattleEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA);
|
||||||
|
expect(SkyBattleEncounter.dialogue).toBeDefined();
|
||||||
|
expect(SkyBattleEncounter.dialogue.intro).toStrictEqual([
|
||||||
|
{
|
||||||
|
text: `${namespace}:intro`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
speaker: `${namespace}:speaker`,
|
||||||
|
text: `${namespace}:intro_dialogue`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(SkyBattleEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`);
|
||||||
|
expect(SkyBattleEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
|
||||||
|
expect(SkyBattleEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`);
|
||||||
|
expect(SkyBattleEncounter.options.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initialize fully", async () => {
|
||||||
|
initSceneWithoutEncounterPhase(scene, defaultParty);
|
||||||
|
scene.currentBattle.mysteryEncounter = SkyBattleEncounter;
|
||||||
|
|
||||||
|
const { onInit } = SkyBattleEncounter;
|
||||||
|
|
||||||
|
expect(SkyBattleEncounter.onInit).toBeDefined();
|
||||||
|
|
||||||
|
SkyBattleEncounter.populateDialogueTokensFromRequirements();
|
||||||
|
const onInitResult = onInit!();
|
||||||
|
const config = SkyBattleEncounter.enemyPartyConfigs[0];
|
||||||
|
|
||||||
|
expect(config).toBeDefined();
|
||||||
|
expect(config.trainerConfig?.trainerType).toBe(TrainerType.SKY_TRAINER);
|
||||||
|
expect(config.trainerConfig?.partyTemplates).toBeDefined();
|
||||||
|
// Allows any gender (randomized)
|
||||||
|
expect(onInitResult).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option 1 - Battle the Sky Trainer", () => {
|
||||||
|
it("should have the correct properties", () => {
|
||||||
|
const option = SkyBattleEncounter.options[0];
|
||||||
|
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
|
||||||
|
expect(option.dialogue).toBeDefined();
|
||||||
|
expect(option.dialogue).toStrictEqual({
|
||||||
|
buttonLabel: `${namespace}:option.1.label`,
|
||||||
|
buttonTooltip: `${namespace}:option.1.tooltip`,
|
||||||
|
selected: [
|
||||||
|
{
|
||||||
|
speaker: `${namespace}:speaker`,
|
||||||
|
text: `${namespace}:option.1.selected`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should start battle against the Sky Trainer", async () => {
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
|
||||||
|
const enemyParty = scene.getEnemyParty();
|
||||||
|
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||||
|
expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.SKY_TRAINER);
|
||||||
|
//Ensure the number of enemy pokemon match our party
|
||||||
|
expect(enemyParty.length).toBe(scene.getPlayerParty().length);
|
||||||
|
expect(enemyParty.every(pkm => POOL_0_POKEMON.includes(pkm.species.speciesId)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should zero disallowed moves' pp", async () => {
|
||||||
|
game.override.moveset([MoveId.DRAGON_CLAW, MoveId.EARTHQUAKE]);
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||||
|
await skipBattleRunMysteryEncounterRewardsPhase(game, false);
|
||||||
|
|
||||||
|
// Only allow acceptable moves (setting available pp to 0)
|
||||||
|
const moveGood = scene.getPlayerParty()[0].getMoveset()[0];
|
||||||
|
const moveBad = scene.getPlayerParty()[0].getMoveset()[1];
|
||||||
|
expect(moveBad.ppUsed).toBe(moveBad.getMovePp());
|
||||||
|
expect(moveGood.ppUsed).toBe(0);
|
||||||
|
|
||||||
|
game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers
|
||||||
|
game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => {
|
||||||
|
game.scene.ui.setCursor(3);
|
||||||
|
game.scene.ui.processInput(Button.ACTION);
|
||||||
|
});
|
||||||
|
await game.phaseInterceptor.run(MysteryEncounterRewardsPhase);
|
||||||
|
|
||||||
|
// Return unacceptable moves' pp
|
||||||
|
const moveBadAfter = scene.getPlayerParty()[0].getMoveset()[1];
|
||||||
|
expect(moveBadAfter.ppUsed).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove ineligeble pokemon from player party", async () => {
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||||
|
await skipBattleRunMysteryEncounterRewardsPhase(game, false);
|
||||||
|
|
||||||
|
// Only allow acceptable pokemon
|
||||||
|
expect(scene.getPlayerParty().length).toBe(3); // we have 2 ineligle pokemon in the default party
|
||||||
|
expect(
|
||||||
|
scene
|
||||||
|
.getPlayerParty()
|
||||||
|
.every(pokemon => ![SpeciesId.WEEDLE, SpeciesId.RATTATA].includes(pokemon.species.speciesId)),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers
|
||||||
|
game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => {
|
||||||
|
game.scene.ui.setCursor(3);
|
||||||
|
game.scene.ui.processInput(Button.ACTION);
|
||||||
|
});
|
||||||
|
await game.phaseInterceptor.run(MysteryEncounterRewardsPhase);
|
||||||
|
|
||||||
|
// Return unacceptable pokemons to party
|
||||||
|
expect(scene.getPlayerParty().length).toBe(defaultParty.length);
|
||||||
|
expect(scene.getPlayerParty()[1].species.speciesId).toBe(SpeciesId.WEEDLE);
|
||||||
|
expect(scene.getPlayerParty()[3].species.speciesId).toBe(SpeciesId.RATTATA);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should let the player learn a Flying move after battle ends", async () => {
|
||||||
|
const selectOptionSpy = vi.spyOn(encounterPhaseUtils, "selectOptionThenPokemon");
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||||
|
await skipBattleRunMysteryEncounterRewardsPhase(game, false);
|
||||||
|
|
||||||
|
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name);
|
||||||
|
game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers
|
||||||
|
game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => {
|
||||||
|
game.phaseInterceptor.superEndPhase();
|
||||||
|
});
|
||||||
|
await game.phaseInterceptor.run(MysteryEncounterRewardsPhase);
|
||||||
|
|
||||||
|
expect(selectOptionSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const optionData = selectOptionSpy.mock.calls[0][0];
|
||||||
|
expect(PHYSICAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[0].label)).toBe(true);
|
||||||
|
expect(SPECIAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[1].label)).toBe(true);
|
||||||
|
expect(SUPPORT_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[2].label)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option 2 - Show off Flying Types", () => {
|
||||||
|
it("should have the correct properties", () => {
|
||||||
|
const option = SkyBattleEncounter.options[1];
|
||||||
|
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
|
||||||
|
expect(option.dialogue).toBeDefined();
|
||||||
|
expect(option.dialogue).toStrictEqual({
|
||||||
|
buttonLabel: `${namespace}:option.2.label`,
|
||||||
|
buttonTooltip: `${namespace}:option.2.tooltip`,
|
||||||
|
disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT be selectable if the player doesn't have enough Flying pokemon", async () => {
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, [
|
||||||
|
SpeciesId.ABRA,
|
||||||
|
SpeciesId.PIDGEY,
|
||||||
|
SpeciesId.SPEAROW,
|
||||||
|
]);
|
||||||
|
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");
|
||||||
|
vi.spyOn(scene.ui, "playError");
|
||||||
|
|
||||||
|
await runSelectMysteryEncounterOption(game, 2);
|
||||||
|
|
||||||
|
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name);
|
||||||
|
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
|
||||||
|
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
|
||||||
|
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should proceed to rewards screen with reward options", async () => {
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
await runMysteryEncounterToEnd(game, 2);
|
||||||
|
|
||||||
|
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.length).toEqual(3);
|
||||||
|
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("QUICK_CLAW");
|
||||||
|
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE");
|
||||||
|
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ULTRA_BALL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should leave encounter without battle", async () => {
|
||||||
|
const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||||
|
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty);
|
||||||
|
await runMysteryEncounterToEnd(game, 2);
|
||||||
|
|
||||||
|
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Option 3 - Reject battle", () => {
|
||||||
|
it("should have the correct properties", async () => {
|
||||||
|
const option = SkyBattleEncounter.options[2];
|
||||||
|
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
|
||||||
|
expect(option.dialogue).toBeDefined();
|
||||||
|
expect(option.dialogue).toStrictEqual({
|
||||||
|
buttonLabel: `${namespace}:option.3.label`,
|
||||||
|
buttonTooltip: `${namespace}:option.3.tooltip`,
|
||||||
|
selected: [
|
||||||
|
{
|
||||||
|
text: `${namespace}:option.3.selected`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should leave encounter without battle", async () => {
|
||||||
|
const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||||
|
|
||||||
|
await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, [SpeciesId.RAYQUAZA]);
|
||||||
|
await runMysteryEncounterToEnd(game, 3);
|
||||||
|
|
||||||
|
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user