From 821e380e571b76771cde22571449a1286dc9508e Mon Sep 17 00:00:00 2001 From: Diogo Diniz Date: Thu, 5 Jun 2025 21:33:16 +0100 Subject: [PATCH 1/7] 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. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Marques --- public/images/trainer/sky_trainer_f.json | 41 ++ public/images/trainer/sky_trainer_f.png | Bin 0 -> 1070 bytes public/images/trainer/sky_trainer_m.json | 41 ++ public/images/trainer/sky_trainer_m.png | Bin 0 -> 1006 bytes .../encounters/sky-battle-encounter.ts | 482 ++++++++++++++++++ .../mystery-encounter-requirements.ts | 48 ++ .../mystery-encounters/mystery-encounters.ts | 44 ++ src/data/trainers/trainer-config.ts | 5 + src/enums/mystery-encounter-type.ts | 3 +- src/enums/trainer-type.ts | 1 + src/plugins/i18n.ts | 20 +- .../encounters/sky-battle-encounter.test.ts | 408 +++++++++++++++ 12 files changed, 1091 insertions(+), 2 deletions(-) create mode 100644 public/images/trainer/sky_trainer_f.json create mode 100644 public/images/trainer/sky_trainer_f.png create mode 100644 public/images/trainer/sky_trainer_m.json create mode 100644 public/images/trainer/sky_trainer_m.png create mode 100644 src/data/mystery-encounters/encounters/sky-battle-encounter.ts create mode 100644 test/mystery-encounter/encounters/sky-battle-encounter.test.ts diff --git a/public/images/trainer/sky_trainer_f.json b/public/images/trainer/sky_trainer_f.json new file mode 100644 index 00000000000..4080abef623 --- /dev/null +++ b/public/images/trainer/sky_trainer_f.json @@ -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$" + } +} diff --git a/public/images/trainer/sky_trainer_f.png b/public/images/trainer/sky_trainer_f.png new file mode 100644 index 0000000000000000000000000000000000000000..ded6662663000105555fb8b955f9791acf2461e9 GIT binary patch literal 1070 zcmeAS@N?(olHy`uVBq!ia0vp^7C`LG!3-q-?KRT_QVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^+^7k6}13By^p1!W^FIlBTM0xbTSAJq(VA|*D;uzv_eD1a0Nplo<7!sOT z0wQnkuKi!0E5zb>VE(eXvuBs>_vJBMbw#1Uc`x5tL*Hkru3N5I9T(BJ&+tFDbjQS7 zxAztEtzV+L!av}(&hZJcGbbyY`#djv_C}Sb-W(mDCoS3M_i}a6St*XTh)>!Q3TF~y zC-MuQ<%sf~I!&nexM@w;#0<9yQ`g_>dd()^WnUle^6H#{vQqi;ANdPncrLC_*s^;T z!=e%vP0cfp>^3wuddBn5{Zco5#~OPc&G|3+`(#gk{r=ru=F6O|_vAztO*!AFc=DXX z@yz`%lP(=t(4&4VJ5E*p+nsGM-+#AYF=$a)c}+{_1fw^zuI2x~POQA%`ovmzzda?|8mXH4c1o zf0v)$q?9!!hgY^PUVCoBJD=yuCON0%4=-=zQ9o-d&*sOg6PxicE!U@_ZR%`i-j{rx z0?UopygKq+;FCM^?wu<|A2ZBR{7}bNJ+-&{LaD-qzoy1d7sO9vyTrh;X7*vfl&;Cb z60xj{tuuCuh4@AAIh3uu_fIY>;pGpZ>G$R>-EDD@w>bV#klX!A{R`+NfgK9-_haUG|LoJ_gT=5QHmlrru;7x2vZ!P1up#q*PQHJy)Ty}v_fSNR21kK2a- zUr)Xsc7-YS>6yE`mph#OJw?3EPknNoy5u|mt}`h~^3FOpSV|V(+xEOcqi5dpJuC86 z)jBjz{^0)c^zfbQU4O*nuig`u*|TlV5}UXU1w8MKS9v6bUz)Aew6lG}bS29Zn)e+t z(|4Ut_CEke{dd zc58_~Fl{rambgZgq$HN4S|t~y0x1R~10z#i15;fivk)T#DPsvQH#I2!eRunL=FlfMSD9OxCEiOsSEx@hkYJasZ$XTAQelF{r5}E)JcgIBl literal 0 HcmV?d00001 diff --git a/public/images/trainer/sky_trainer_m.json b/public/images/trainer/sky_trainer_m.json new file mode 100644 index 00000000000..9cf750481b2 --- /dev/null +++ b/public/images/trainer/sky_trainer_m.json @@ -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$" + } +} diff --git a/public/images/trainer/sky_trainer_m.png b/public/images/trainer/sky_trainer_m.png new file mode 100644 index 0000000000000000000000000000000000000000..0a817932b05e7eb45b5c9aa41aefe940825bd96e GIT binary patch literal 1006 zcmeAS@N?(olHy`uVBq!ia0vp^20-l3!3-p4t=P5@NGT+H1o<*BRH-pAG_)`<`~nI! zykKA`1uA@%z`$TNgMmT3V9u^U8=!<)fKP}kkoL-|KK|-o>)c(2hK4|Hb91+sSJeOi z|M%`a+7Rh+FDS~>+Tz}l>i<{w?yXL$_KG?i6?Kr~p(0QzV@Z%-FoVOh8)+a;lDE4H zN87!rRX`4ViKnkC`%6|S5m6rf@0Fhz7?@f-T^vI^j&Hr*-PdFwz#4Gs%$LT7U;piQ zf6ln7B(kg1w`X<*KXYo-jl>L|<;gp&=17;usLHTUIrt=M-ic~?rv#?ypENRMU6wn2 ziue@MY*+uKjy-|buC@PSy(ACoi*VjO{hGgg?IJiAWiFC>@U|>@BWuTHAEEh@55(>S zt?@qbgy{vRe^snm&2Z}^f8XrmT@jLk z=bwMxc<1@uKM#^lxPF=`sH-#Wcio+3L3;N&3UxL_yfDyDm=br)iDQMJdE(oA6$OSl zj3>0+K1FWn+99~3t;XZiN%jfP>bUnaA5)lr;I*-vTb%2@`F6SDeUCRRFjRQ%`Xl0+ z;Q8WDocs(P&Tf(27x$MnP1#mCJIXo2Van{otMzK+|8v~<*lD1;UwvQ0SyPKVF&j3u zCyN>xJ^uZO3uy>vTfgkF>`v)D%dR_+YbVY8p@ZD`3|a5}PG&q!|GI>t$i1de)73Y2Z6)=R zt8Xno?-F{@>kMwFx^mZVxG z7o`Fz1|tI_Q(Xg7T_dv)BLgcVLn}igZ36=<1B1`UANiqZ$jwj5OsmALp=nkWFvT-y hz-=hW%uOvWNz5(4t> = new Map(); + +/** + * 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 { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO explain + return new Promise(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(); + }); +} diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index a6e6e84846f..0913670eb9b 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1213,3 +1213,51 @@ export class WeightRequirement extends EncounterPokemonRequirement { 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); + } +} diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 5ee289a6c56..40afad7edff 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -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 { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter"; import { getBiomeName } from "#app/data/balance/biomes"; +import { SkyBattleEncounter } from "./encounters/sky-battle-encounter"; export const EXTREME_ENCOUNTER_BIOMES = [ BiomeId.SEA, @@ -135,6 +136,36 @@ export const CIVILIZATION_ENCOUNTER_BIOMES = [ 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: { [encounterType: number]: MysteryEncounter; } = {}; @@ -162,6 +193,8 @@ const civilizationBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.GLOBAL_TRADE_SYSTEM, ]; +const openSkyBiomeEncounters: MysteryEncounterType[] = [MysteryEncounterType.SKY_BATTLE]; + /** * 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.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter; + allMysteryEncounters[MysteryEncounterType.SKY_BATTLE] = SkyBattleEncounter; // Add extreme encounters to biome map 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 let _encounterBiomeTableLog = ""; mysteryEncountersByBiome.forEach((biomeEncounters, biome) => { diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 063dddafee8..9381733881e 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -6045,4 +6045,9 @@ export const trainerConfigs: TrainerConfigs = { .setVictoryBgm("mystery_encounter_weird_dream") .setLocalizedName("Future Self F") .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)), }; diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index b973652b113..546549e40a5 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -29,5 +29,6 @@ export enum MysteryEncounterType { FUN_AND_GAMES, UNCOMMON_BREED, GLOBAL_TRADE_SYSTEM, - THE_EXPERT_POKEMON_BREEDER + THE_EXPERT_POKEMON_BREEDER, + SKY_BATTLE, } diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index e22dc5d81c7..130f338dcb6 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -118,6 +118,7 @@ export enum TrainerType { EXPERT_POKEMON_BREEDER, FUTURE_SELF_M, FUTURE_SELF_F, + SKY_TRAINER, BROCK = 200, MISTY, diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 515d9aec528..2a9a072fd9e 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -174,7 +174,24 @@ export async function initI18n(): Promise { "es-MX": ["es-ES", "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: { loadPath(lng: string, [ns]: string[]) { let fileName: string; @@ -276,6 +293,7 @@ export async function initI18n(): Promise { "mysteryEncounters/theWinstrateChallenge", "mysteryEncounters/teleportingHijinks", "mysteryEncounters/bugTypeSuperfan", + "mysteryEncounters/skyBattle", "mysteryEncounters/funAndGames", "mysteryEncounters/uncommonBreed", "mysteryEncounters/globalTradeSystem", diff --git a/test/mystery-encounter/encounters/sky-battle-encounter.test.ts b/test/mystery-encounter/encounters/sky-battle-encounter.test.ts new file mode 100644 index 00000000000..accc942f94c --- /dev/null +++ b/test/mystery-encounter/encounters/sky-battle-encounter.test.ts @@ -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.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(); + }); + }); +}); From f0d6b88a172ef5d92cc2179e668fcb9dfd14d5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Serrado=20Marques?= Date: Fri, 6 Jun 2025 11:40:00 +0100 Subject: [PATCH 2/7] Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/mystery-encounters/mystery-encounters.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 40afad7edff..8064fb5de35 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -193,7 +193,8 @@ const civilizationBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.GLOBAL_TRADE_SYSTEM, ]; -const openSkyBiomeEncounters: MysteryEncounterType[] = [MysteryEncounterType.SKY_BATTLE]; +// Temporarily disabled pending ME rebalancing +// const openSkyBiomeEncounters: MysteryEncounterType[] = [MysteryEncounterType.SKY_BATTLE]; /** * To add an encounter to every biome possible, use this array @@ -290,7 +291,8 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter; - allMysteryEncounters[MysteryEncounterType.SKY_BATTLE] = SkyBattleEncounter; + // Temporarily disabled pending ME rebalancing + // allMysteryEncounters[MysteryEncounterType.SKY_BATTLE] = SkyBattleEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { @@ -330,6 +332,8 @@ export function initMysteryEncounters() { }); // add open sky encounters to biome map + // Temporarily disabled pending ME rebalancing + /* openSkyBiomeEncounters.forEach(encounter => { OPEN_SKY_BIOMES.forEach(biome => { const encountersForBiome = mysteryEncountersByBiome.get(biome); @@ -338,6 +342,7 @@ export function initMysteryEncounters() { } }); }); + */ // Add ANY biome encounters to biome map let _encounterBiomeTableLog = ""; From 50816b41f5b476500052ac22b016e796ff22a3e7 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 6 Jun 2025 03:48:50 -0700 Subject: [PATCH 3/7] Disable tests --- test/mystery-encounter/encounters/sky-battle-encounter.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/mystery-encounter/encounters/sky-battle-encounter.test.ts b/test/mystery-encounter/encounters/sky-battle-encounter.test.ts index accc942f94c..7838a11f3cd 100644 --- a/test/mystery-encounter/encounters/sky-battle-encounter.test.ts +++ b/test/mystery-encounter/encounters/sky-battle-encounter.test.ts @@ -148,7 +148,8 @@ const SPECIAL_TUTOR_MOVES = [MoveId.AEROBLAST, MoveId.AIR_SLASH, MoveId.HURRICAN const SUPPORT_TUTOR_MOVES = [MoveId.FEATHER_DANCE, MoveId.ROOST, MoveId.PLUCK, MoveId.TAILWIND]; -describe("Sky Battle - Mystery Encounter", () => { +// Re-enable when the ME is re-enabled +describe.todo("Sky Battle - Mystery Encounter", () => { let phaserGame: Phaser.Game; let game: GameManager; let scene: BattleScene; From 656a21882ec6e3a197564dab81216c2f102ef985 Mon Sep 17 00:00:00 2001 From: Diogo Diniz Date: Thu, 12 Jun 2025 22:30:48 +0100 Subject: [PATCH 4/7] Expanded eligible pokemon pool Added distinct keys for female trainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Serrado Marques --- .../encounters/sky-battle-encounter.ts | 117 +++++++++++++++++- .../encounters/sky-battle-encounter.test.ts | 85 +++++++++++-- 2 files changed, 189 insertions(+), 13 deletions(-) diff --git a/src/data/mystery-encounters/encounters/sky-battle-encounter.ts b/src/data/mystery-encounters/encounters/sky-battle-encounter.ts index b5fab3bae81..36c30b9ed25 100644 --- a/src/data/mystery-encounters/encounters/sky-battle-encounter.ts +++ b/src/data/mystery-encounters/encounters/sky-battle-encounter.ts @@ -18,18 +18,18 @@ import { 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 type { PlayerPokemon } from "#app/field/pokemon"; +import { PokemonMove } from "#app/data/moves/pokemon-move"; 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 { allMoves, modifierTypes } 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"; @@ -45,15 +45,18 @@ 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} + * Also pokemon that are expected to fly (e.g beedril and mew) */ const POOL_0_POKEMON = [ SpeciesId.CHARIZARD, SpeciesId.BUTTERFREE, + SpeciesId.BEEDRILL, SpeciesId.PIDGEOTTO, SpeciesId.PIDGEOT, SpeciesId.FEAROW, SpeciesId.ZUBAT, SpeciesId.GOLBAT, + SpeciesId.VENOMOTH, SpeciesId.HAUNTER, SpeciesId.KOFFING, SpeciesId.WEEZING, @@ -64,6 +67,8 @@ const POOL_0_POKEMON = [ SpeciesId.ZAPDOS, SpeciesId.MOLTRES, SpeciesId.DRAGONITE, + SpeciesId.MEWTWO, // ? + SpeciesId.MEW, SpeciesId.NOCTOWL, SpeciesId.LEDYBA, SpeciesId.LEDIAN, @@ -76,17 +81,21 @@ const POOL_0_POKEMON = [ SpeciesId.YANMA, SpeciesId.MISDREAVUS, SpeciesId.UNOWN, + SpeciesId.FORRETRESS, // ? SpeciesId.GLIGAR, SpeciesId.MANTINE, SpeciesId.SKARMORY, SpeciesId.LUGIA, SpeciesId.HO_OH, + SpeciesId.CELEBI, SpeciesId.BEAUTIFLY, + SpeciesId.DUSTOX, SpeciesId.SWELLOW, SpeciesId.WINGULL, SpeciesId.PELIPPER, SpeciesId.MASQUERAIN, SpeciesId.NINJASK, + SpeciesId.SHEDINJA, // ? SpeciesId.VIBRAVA, SpeciesId.FLYGON, SpeciesId.SWABLU, @@ -98,10 +107,14 @@ const POOL_0_POKEMON = [ SpeciesId.DUSKULL, SpeciesId.TROPIUS, SpeciesId.CHIMECHO, + SpeciesId.GLALIE, // ? SpeciesId.SALAMENCE, + SpeciesId.METANG, + SpeciesId.METAGROSS, // ? SpeciesId.LATIAS, SpeciesId.LATIOS, SpeciesId.RAYQUAZA, + SpeciesId.JIRACHI, SpeciesId.STARAVIA, SpeciesId.STARAPTOR, SpeciesId.MOTHIM, @@ -116,38 +129,95 @@ const POOL_0_POKEMON = [ SpeciesId.BRONZONG, SpeciesId.CARNIVINE, SpeciesId.MANTYKE, + SpeciesId.MAGNEZONE, // ? SpeciesId.TOGEKISS, SpeciesId.YANMEGA, SpeciesId.GLISCOR, + SpeciesId.DUSKNOIR, // ? SpeciesId.ROTOM, SpeciesId.UXIE, SpeciesId.MESPRIT, SpeciesId.AZELF, + SpeciesId.GIRATINA, SpeciesId.CRESSELIA, + SpeciesId.ARCEUS, SpeciesId.TRANQUILL, SpeciesId.UNFEZANT, SpeciesId.WOOBAT, SpeciesId.SWOOBAT, SpeciesId.SIGILYPH, SpeciesId.ARCHEOPS, + SpeciesId.SOLOSIS, + SpeciesId.DUOSION, + SpeciesId.REUNICLUS, SpeciesId.SWANNA, + SpeciesId.VANILLISH, + SpeciesId.VANILLUXE, SpeciesId.EMOLGA, SpeciesId.TYNAMO, SpeciesId.EELEKTRIK, SpeciesId.EELEKTROSS, + SpeciesId.LAMPENT, + SpeciesId.CHANDELURE, SpeciesId.CRYOGONAL, SpeciesId.BRAVIARY, SpeciesId.MANDIBUZZ, SpeciesId.HYDREIGON, + SpeciesId.VOLCARONA, SpeciesId.TORNADUS, SpeciesId.THUNDURUS, + SpeciesId.RESHIRAM, + SpeciesId.ZEKROM, SpeciesId.LANDORUS, SpeciesId.FLETCHINDER, SpeciesId.TALONFLAME, SpeciesId.VIVILLON, + SpeciesId.FLOETTE, + SpeciesId.FLORGES, + SpeciesId.HAWLUCHA, // ? SpeciesId.NOIBAT, SpeciesId.NOIVERN, SpeciesId.YVELTAL, + SpeciesId.DARTRIX, + SpeciesId.DECIDUEYE, //? + SpeciesId.TRUMBEAK, + SpeciesId.TOUCANNON, + SpeciesId.VIKAVOLT, + SpeciesId.ORICORIO, + SpeciesId.RIBOMBEE, + SpeciesId.COMFEY, //? + SpeciesId.MINIOR, + SpeciesId.TAPU_KOKO, + SpeciesId.TAPU_LELE, + SpeciesId.TAPU_BULU, + SpeciesId.TAPU_FINI, + SpeciesId.LUNALA, + SpeciesId.NIHILEGO, + SpeciesId.BUZZWOLE, + SpeciesId.CELESTEELA, + SpeciesId.NECROZMA, + SpeciesId.POIPOLE, + SpeciesId.NAGANADEL, + SpeciesId.CORVISQUIRE, + SpeciesId.CORVIKNIGHT, + SpeciesId.ORBEETLE, + SpeciesId.FLAPPLE, + SpeciesId.CRAMORANT, + SpeciesId.FROSMOTH, + SpeciesId.DRAKLOAK, + SpeciesId.DRAGAPULT, + SpeciesId.ETERNATUS, + SpeciesId.ENAMORUS, + SpeciesId.SQUAWKABILLY, + SpeciesId.WATTREL, + SpeciesId.KILOWATTREL, + SpeciesId.BOMBIRDIER, + SpeciesId.FLAMIGO, + SpeciesId.FLUTTER_MANE, + SpeciesId.IRON_JUGULIS, + SpeciesId.ROARING_MOON, + SpeciesId.MIRAIDON, + SpeciesId.KORAIDON, ]; const PHYSICAL_TUTOR_MOVES = [ @@ -260,6 +330,42 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with }, ]; + const intro = [ + { + text: `${namespace}:intro` + female ? "_f" : "", + }, + { + speaker: `${namespace}:speaker`, + text: `${namespace}:intro_dialogue` + female ? "_f" : "", + }, + ]; + const title = `${namespace}:title` + female ? "_f" : ""; + const description = `${namespace}:description` + female ? "_f" : ""; + const outro = [ + { + text: `${namespace}:outro` + female ? "_f" : "", + }, + ]; + + encounter.dialogue = { ...encounter.dialogue, intro: intro }; + let encounterOptionsDialogue = encounter.dialogue.encounterOptionsDialogue ?? {}; + encounter.dialogue = { + ...encounter.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + title, + }, + }; + encounterOptionsDialogue = encounter.dialogue.encounterOptionsDialogue ?? {}; + encounter.dialogue = { + ...encounter.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + description, + }, + }; + encounter.dialogue = { ...encounter.dialogue, outro: outro }; + return true; }) .setLocalizationKey(`${namespace}`) @@ -409,7 +515,8 @@ function doFlyingTypeTutor(): Promise { // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO explain return new Promise(async resolve => { const moveOptions = globalScene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; - await showEncounterDialogue(`${namespace}:battle_won`, `${namespace}:speaker`); + const female = globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0].female; //TODO: Is this [0] correct enought? + await showEncounterDialogue(`${namespace}:battle_won` + female ? "_f" : "", `${namespace}:speaker`); const overlayScale = 1; const moveInfoOverlay = new MoveInfoOverlay({ @@ -456,7 +563,7 @@ function doFlyingTypeTutor(): Promise { } // Option select complete, handle if they are learning a move if (result && result.selectedOptionIndex < moveOptions.length) { - globalScene.unshiftPhase( + globalScene.phaseManager.unshiftPhase( new LearnMovePhase(result.selectedPokemonIndex, moveOptions[result.selectedOptionIndex].moveId), ); } diff --git a/test/mystery-encounter/encounters/sky-battle-encounter.test.ts b/test/mystery-encounter/encounters/sky-battle-encounter.test.ts index 7838a11f3cd..0e88227b244 100644 --- a/test/mystery-encounter/encounters/sky-battle-encounter.test.ts +++ b/test/mystery-encounter/encounters/sky-battle-encounter.test.ts @@ -11,7 +11,7 @@ import { } 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 { PokemonMove } from "#app/data/moves/pokemon-move"; import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -33,11 +33,13 @@ const defaultWave = 52; const POOL_0_POKEMON = [ SpeciesId.CHARIZARD, SpeciesId.BUTTERFREE, + SpeciesId.BEEDRILL, SpeciesId.PIDGEOTTO, SpeciesId.PIDGEOT, SpeciesId.FEAROW, SpeciesId.ZUBAT, SpeciesId.GOLBAT, + SpeciesId.VENOMOTH, SpeciesId.HAUNTER, SpeciesId.KOFFING, SpeciesId.WEEZING, @@ -48,6 +50,8 @@ const POOL_0_POKEMON = [ SpeciesId.ZAPDOS, SpeciesId.MOLTRES, SpeciesId.DRAGONITE, + SpeciesId.MEWTWO, // ? + SpeciesId.MEW, SpeciesId.NOCTOWL, SpeciesId.LEDYBA, SpeciesId.LEDIAN, @@ -60,17 +64,21 @@ const POOL_0_POKEMON = [ SpeciesId.YANMA, SpeciesId.MISDREAVUS, SpeciesId.UNOWN, + SpeciesId.FORRETRESS, // ? SpeciesId.GLIGAR, SpeciesId.MANTINE, SpeciesId.SKARMORY, SpeciesId.LUGIA, SpeciesId.HO_OH, + SpeciesId.CELEBI, SpeciesId.BEAUTIFLY, + SpeciesId.DUSTOX, SpeciesId.SWELLOW, SpeciesId.WINGULL, SpeciesId.PELIPPER, SpeciesId.MASQUERAIN, SpeciesId.NINJASK, + SpeciesId.SHEDINJA, // ? SpeciesId.VIBRAVA, SpeciesId.FLYGON, SpeciesId.SWABLU, @@ -82,10 +90,14 @@ const POOL_0_POKEMON = [ SpeciesId.DUSKULL, SpeciesId.TROPIUS, SpeciesId.CHIMECHO, + SpeciesId.GLALIE, // ? SpeciesId.SALAMENCE, + SpeciesId.METANG, + SpeciesId.METAGROSS, // ? SpeciesId.LATIAS, SpeciesId.LATIOS, SpeciesId.RAYQUAZA, + SpeciesId.JIRACHI, SpeciesId.STARAVIA, SpeciesId.STARAPTOR, SpeciesId.MOTHIM, @@ -100,38 +112,95 @@ const POOL_0_POKEMON = [ SpeciesId.BRONZONG, SpeciesId.CARNIVINE, SpeciesId.MANTYKE, + SpeciesId.MAGNEZONE, // ? SpeciesId.TOGEKISS, SpeciesId.YANMEGA, SpeciesId.GLISCOR, + SpeciesId.DUSKNOIR, // ? SpeciesId.ROTOM, SpeciesId.UXIE, SpeciesId.MESPRIT, SpeciesId.AZELF, + SpeciesId.GIRATINA, SpeciesId.CRESSELIA, + SpeciesId.ARCEUS, SpeciesId.TRANQUILL, SpeciesId.UNFEZANT, SpeciesId.WOOBAT, SpeciesId.SWOOBAT, SpeciesId.SIGILYPH, SpeciesId.ARCHEOPS, + SpeciesId.SOLOSIS, + SpeciesId.DUOSION, + SpeciesId.REUNICLUS, SpeciesId.SWANNA, + SpeciesId.VANILLISH, + SpeciesId.VANILLUXE, SpeciesId.EMOLGA, SpeciesId.TYNAMO, SpeciesId.EELEKTRIK, SpeciesId.EELEKTROSS, + SpeciesId.LAMPENT, + SpeciesId.CHANDELURE, SpeciesId.CRYOGONAL, SpeciesId.BRAVIARY, SpeciesId.MANDIBUZZ, SpeciesId.HYDREIGON, + SpeciesId.VOLCARONA, SpeciesId.TORNADUS, SpeciesId.THUNDURUS, + SpeciesId.RESHIRAM, + SpeciesId.ZEKROM, SpeciesId.LANDORUS, SpeciesId.FLETCHINDER, SpeciesId.TALONFLAME, SpeciesId.VIVILLON, + SpeciesId.FLOETTE, + SpeciesId.FLORGES, + SpeciesId.HAWLUCHA, // ? SpeciesId.NOIBAT, SpeciesId.NOIVERN, SpeciesId.YVELTAL, + SpeciesId.DARTRIX, + SpeciesId.DECIDUEYE, //? + SpeciesId.TRUMBEAK, + SpeciesId.TOUCANNON, + SpeciesId.VIKAVOLT, + SpeciesId.ORICORIO, + SpeciesId.RIBOMBEE, + SpeciesId.COMFEY, //? + SpeciesId.MINIOR, + SpeciesId.TAPU_KOKO, + SpeciesId.TAPU_LELE, + SpeciesId.TAPU_BULU, + SpeciesId.TAPU_FINI, + SpeciesId.LUNALA, + SpeciesId.NIHILEGO, + SpeciesId.BUZZWOLE, + SpeciesId.CELESTEELA, + SpeciesId.NECROZMA, + SpeciesId.POIPOLE, + SpeciesId.NAGANADEL, + SpeciesId.CORVISQUIRE, + SpeciesId.CORVIKNIGHT, + SpeciesId.ORBEETLE, + SpeciesId.FLAPPLE, + SpeciesId.CRAMORANT, + SpeciesId.FROSMOTH, + SpeciesId.DRAKLOAK, + SpeciesId.DRAGAPULT, + SpeciesId.ETERNATUS, + SpeciesId.ENAMORUS, + SpeciesId.SQUAWKABILLY, + SpeciesId.WATTREL, + SpeciesId.KILOWATTREL, + SpeciesId.BOMBIRDIER, + SpeciesId.FLAMIGO, + SpeciesId.FLUTTER_MANE, + SpeciesId.IRON_JUGULIS, + SpeciesId.ROARING_MOON, + SpeciesId.MIRAIDON, + SpeciesId.KORAIDON, ]; const PHYSICAL_TUTOR_MOVES = [ @@ -239,7 +308,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); const enemyParty = scene.getEnemyParty(); - expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.phaseManager.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); @@ -250,7 +319,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { 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); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); await skipBattleRunMysteryEncounterRewardsPhase(game, false); // Only allow acceptable moves (setting available pp to 0) @@ -274,7 +343,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { 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); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); await skipBattleRunMysteryEncounterRewardsPhase(game, false); // Only allow acceptable pokemon @@ -304,7 +373,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game, false); - expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name); game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => { game.phaseInterceptor.superEndPhase(); @@ -339,7 +408,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { ]); await game.phaseInterceptor.to(MysteryEncounterPhase, false); - const encounterPhase = scene.getCurrentPhase(); + const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; vi.spyOn(mysteryEncounterPhase, "continueEncounter"); @@ -348,7 +417,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { await runSelectMysteryEncounterOption(game, 2); - expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.phaseManager.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(); @@ -358,7 +427,7 @@ describe.todo("Sky Battle - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.SKY_BATTLE, defaultParty); await runMysteryEncounterToEnd(game, 2); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); From bcb38af8a169ccca663ed2a6ae95884fc91555e3 Mon Sep 17 00:00:00 2001 From: Diogo Diniz Date: Sat, 21 Jun 2025 23:27:29 +0100 Subject: [PATCH 5/7] increase eligible pokemon pool add form specific requirements adjust sky trainer images todo: create different enemy teams, add trainer name logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Serrado Marques --- public/images/trainer/sky_trainer_f.json | 8 +- public/images/trainer/sky_trainer_f.png | Bin 1070 -> 952 bytes public/images/trainer/sky_trainer_m.json | 8 +- public/images/trainer/sky_trainer_m.png | Bin 1006 -> 875 bytes .../encounters/sky-battle-encounter.ts | 183 ++++++++++++++---- .../mystery-encounter-requirements.ts | 41 ++++ src/data/trainers/trainer-config.ts | 2 +- src/enums/species-form-key.ts | 5 + 8 files changed, 201 insertions(+), 46 deletions(-) diff --git a/public/images/trainer/sky_trainer_f.json b/public/images/trainer/sky_trainer_f.json index 4080abef623..2491198fafe 100644 --- a/public/images/trainer/sky_trainer_f.json +++ b/public/images/trainer/sky_trainer_f.json @@ -5,7 +5,7 @@ "format": "RGBA8888", "size": { "w": 56, - "h": 67 + "h": 82 }, "scale": 1, "frames": [ @@ -15,19 +15,19 @@ "trimmed": true, "sourceSize": { "w": 56, - "h": 67 + "h": 82 }, "spriteSourceSize": { "x": 0, "y": 0, "w": 56, - "h": 67 + "h": 82 }, "frame": { "x": 0, "y": 0, "w": 56, - "h": 67 + "h": 82 } } ] diff --git a/public/images/trainer/sky_trainer_f.png b/public/images/trainer/sky_trainer_f.png index ded6662663000105555fb8b955f9791acf2461e9..6581d58de7dfbc3a2c112ce8c5ef7c28a7dc7c53 100644 GIT binary patch delta 843 zcmV-R1GN0E2)GB37bj8(0{{R3HaF)G00001b5ch_0olnce*gdg1ZP1_K>z@;j|==^ z1(6{y3qnv#R7C&)00000kySi@v68J@00001bW%=J06^y0W&i*H32;bRa{vGf6951U z69E94oEQKA0>(*1K~z}7?bq#YLzQlzz!RbTPK-8M{j4d;>5bJS1fVCM54n3O9`=(q4_ste>N(}xVLnD*D<+d!L>`(8E! z{RXx!ZeemRCtSmY8{&F(W{7?b7j@!3r25q;ZrId{+yHAxGN0pI<;G>`cOdceH3*g9 z590=M11MZBjyRQnC1SUQ*)3ppZ@L`|T}{|FKy{_w{W-LOt&>|EvHRAlFrltX9NXA; zAt($LQcm<{ICxD4$BkX|W9to)OY?xk9pd^dGYM+qV{T_X_rN_>sH0n7{EGzw!U#AA0~X VyHh_BkN^M*002ovPDHLkV1gQvg8={l delta 962 zcmdnNzK&ypIF~aAGXn#|zrAL96Yca_Z328kTqjo8)W46Zoe30YED7=pW^j0RBMrn! z@^*LO%in9@59F|yc>21szhspX5#`bUUipcEfoY$oi(`n#@wwM}C(TjdVMu6V35dMC zyY_#1t`Ljkf%(hk&YoSi-Mq4Sk=fx^B5KEwaq(j60T-QHKs zw|bKsL6InFne52yYa}LKd z_rFZKbYMY``myXdRrzmsw!M7+-GarSMP=nREu9mL-psm||NlC%@_O%U{&|D3V4;QQ z?w`FzK^s?1FkHUWzNED6xvt8}?+QmZl?Yx+oU}MrIX)qmsf@pDmRR>H#lK6n)_nf5 zR&ZVuTWYpmw8GUS+3TC?P5XAcif*lKJ5|{x)!(t&MccIH(TYo7ZW>j+e4Zd13}PwvdScditD%rHmsLmglB)ZXq3r3x4Rni@Y{5I>FW5(CGY*@x@>Qo1G! zOT@A+w$9is7UCDd=TNrt-aomlgqJ^rrr(>lbhpJp-s1Q}L2mae^)IAz7+yTM>pXMY zt+%f|`dEs3#dVw}ax&#Uo5N+OQOdaYT);Eq2TNZb6wgoI)pS0V_5Kc_UF8>4J#HKR ze?9qn*cGPOr)Tc&UhZ)A_Z0CuKlRCV>h+TE{JYMiB*{DL++ZnLd~e(H292J1&-bjz zS5@oKIQfJ7$J4`iu6O+rlfQaTSZ2?*IZJHfHWcu@H(uqD6n<&8R@2V*3DcDy$LklegDIfnan@p->sYZpFw_} z=G(0$`t`sR&Y)W28c~vxSdwa$T$Bo=7>o>zOmz)Rb&bqIj0~)d46O`}v<(cb3=BRW zf8>XvAvZrIGp!Q0hNf9jz#PM%0k@$fGdH!kBr&%DqDKXrEm!-ibwTd(boFyt=akR{ E0A(4W?f?J) diff --git a/public/images/trainer/sky_trainer_m.json b/public/images/trainer/sky_trainer_m.json index 9cf750481b2..67c0d622351 100644 --- a/public/images/trainer/sky_trainer_m.json +++ b/public/images/trainer/sky_trainer_m.json @@ -5,7 +5,7 @@ "format": "RGBA8888", "size": { "w": 48, - "h": 79 + "h": 84 }, "scale": 1, "frames": [ @@ -15,19 +15,19 @@ "trimmed": true, "sourceSize": { "w": 48, - "h": 79 + "h": 84 }, "spriteSourceSize": { "x": 0, "y": 0, "w": 48, - "h": 79 + "h": 84 }, "frame": { "x": 0, "y": 0, "w": 48, - "h": 79 + "h": 84 } } ] diff --git a/public/images/trainer/sky_trainer_m.png b/public/images/trainer/sky_trainer_m.png index 0a817932b05e7eb45b5c9aa41aefe940825bd96e..2d722c8e58d2c9cdaffd034defe3e9484c19a2e2 100644 GIT binary patch delta 771 zcmV+e1N{8%2kQoq7bjE*0{{R3^SMO%00001b5ch_0olnce*gdg1ZP1_K>z@;j|==^ z1(6{y2suzpR7C&)F_Bd^e?IdjI{*Lx0d!JMQvg8b*k%9#010qNS#tmY3ljhU3ljkV znw%H_00M$ZL_t(o!{wISuA?vvMcKhtE);0{{~tTH6KLUZNk%%Fhp~kE(7o7+V>@)+ zML3JDHk|7%Y7vnWYY0-O2fX(o1VN}u&#JZns7B9=UOe#qr}qZ{e{X?r!pG=;FdP=Z zezt)3F-5=Ahim~11GWG^9uFb*0P`?H<)Z++2OcSIEj0P#Fu7?Qr*Xn&MI7CO1=xBx z1CZu*pD_yL zjVR~o)4p+>h!yGsi(UyImp+m_C!hEPAT7wKy%_yE2~yhYe{*Ux#0wDhaHQ+P&-b4J z`TYQ=X?_*D(DR5c2FU_ZES# z0m#%_^kU%(;ZDc6T@s64>l8|y&;9(>hV>?O=yHpm02{q++LC(;Wc^w=`fhxzZuLf# z-5g#+HedRU_wAi1y^OB@!`6lcTJ-xB)d0Vz&p?ZwMcVXc;JQWtjli!O{KXEa2~a&X zt%zEI#=34U3$)f9sM&$;zEF?W)Kp2h`DXs#;}5T!9ra$0tR(;d002ovPDHLkV1h)U BP{{xQ delta 903 zcmaFO_KtmmIF~;MGXn#|tQFf9PPEf!H4E?wah+IdR)3J=p(0S6u_VYZn8D%MjWiG^ z$=lt9qwU_*DjbEHdSRAtzw9DEWr??kn{Qv%cUPa2uB zF3X)h)kl1aX|}8XQpcXaYuDO;v0jpg^+h=Eo_@_=zIG9ui!v8UJ$PG|ypgrzvX9Vw z$p>P0g4TGScyj*oqiL0K&stX*A9|5@V!^tHA^}m?zn;5q63V3fJoyNh#RIO1j7wAJ z*&S@CS*N1Pq;JCXg44e$)~sf@b&|ht z_VKO=Nx}2aKX1JA{O+FzNhe%C%@owtnfANx&axoA`y7Qj8zNp9=qF5xJLbf(LeM<% zZN7>E!yLvF+HRjBw{-0g+|gF!@#!S{glBc!`DouT=Blg8x|NU zJa_#OaZT`i@h47x29J7Yx5(~``^%c9Y^$6d*Bm?~d9FJM9QY}w5ggt4O!(3F1MgU0 zJ)3s$r19yg&%Bw0Vi|Wv>NijFXL_>4&*5ZtBlm%@+0XViwB#;09oepDR4+Gg9pj|b z8om^9Zp-;cpIO#2Jo&NYfO%u?F-0{#>kVs}qLtp?+jMv07t_prtW9t4{@Tuv!~7_A z{ga(7rw_^IJ^nR8qw;02P~o2Ux1*!~1%?^(Zgb&xn0meY+DTiMY4xUkGPCb3_4};# z>FOK1wvu|u)wha#%m`+to zTq8?Mnk)f5Lk+y+>m4U(M$%!rtqat{;OXk;vd$@?2>@T=i0l9W diff --git a/src/data/mystery-encounters/encounters/sky-battle-encounter.ts b/src/data/mystery-encounters/encounters/sky-battle-encounter.ts index 36c30b9ed25..2d5dc798a47 100644 --- a/src/data/mystery-encounters/encounters/sky-battle-encounter.ts +++ b/src/data/mystery-encounters/encounters/sky-battle-encounter.ts @@ -16,6 +16,9 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { AbilityRequirement, AnyCombinationPokemonRequirement, + CombinationPokemonRequirement, + FormPokemonRequirement, + SpeciesRequirement, TypeRequirement, } from "../mystery-encounter-requirements"; import { PokemonType } from "#enums/pokemon-type"; @@ -36,66 +39,75 @@ import { getRandomPartyMemberFunc, type TrainerConfig, trainerConfigs } from "#a import { TrainerType } from "#enums/trainer-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { TrainerSlot } from "#enums/trainer-slot"; +import { SpeciesFormKey } from "#enums/species-form-key"; /** 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} - * Also pokemon that are expected to fly (e.g beedril and mew) - */ -const POOL_0_POKEMON = [ - SpeciesId.CHARIZARD, +const POOL_ALL_FORMS = [ SpeciesId.BUTTERFREE, SpeciesId.BEEDRILL, + SpeciesId.PIDGEY, SpeciesId.PIDGEOTTO, SpeciesId.PIDGEOT, + SpeciesId.SPEAROW, SpeciesId.FEAROW, SpeciesId.ZUBAT, SpeciesId.GOLBAT, SpeciesId.VENOMOTH, + SpeciesId.MAGNEMITE, + SpeciesId.MAGNETON, + SpeciesId.GASTLY, SpeciesId.HAUNTER, + SpeciesId.GENGAR, SpeciesId.KOFFING, SpeciesId.WEEZING, SpeciesId.SCYTHER, SpeciesId.GYARADOS, + SpeciesId.PORYGON, SpeciesId.AERODACTYL, SpeciesId.ARTICUNO, SpeciesId.ZAPDOS, SpeciesId.MOLTRES, SpeciesId.DRAGONITE, - SpeciesId.MEWTWO, // ? + SpeciesId.MEWTWO, SpeciesId.MEW, + SpeciesId.HOOTHOOT, SpeciesId.NOCTOWL, SpeciesId.LEDYBA, SpeciesId.LEDIAN, SpeciesId.CROBAT, SpeciesId.TOGETIC, + SpeciesId.NATU, SpeciesId.XATU, SpeciesId.HOPPIP, SpeciesId.SKIPLOOM, SpeciesId.JUMPLUFF, SpeciesId.YANMA, + SpeciesId.MURKROW, SpeciesId.MISDREAVUS, - SpeciesId.UNOWN, - SpeciesId.FORRETRESS, // ? + SpeciesId.UNOWN, // ALL (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, !, ?) SpeciesId.GLIGAR, + SpeciesId.DELIBIRD, SpeciesId.MANTINE, SpeciesId.SKARMORY, + SpeciesId.PORYGON2, SpeciesId.LUGIA, SpeciesId.HO_OH, SpeciesId.CELEBI, SpeciesId.BEAUTIFLY, SpeciesId.DUSTOX, + SpeciesId.TAILLOW, SpeciesId.SWELLOW, SpeciesId.WINGULL, SpeciesId.PELIPPER, SpeciesId.MASQUERAIN, SpeciesId.NINJASK, - SpeciesId.SHEDINJA, // ? + SpeciesId.SHEDINJA, + SpeciesId.VOLBEAT, + SpeciesId.ILLUMISE, SpeciesId.VIBRAVA, SpeciesId.FLYGON, SpeciesId.SWABLU, @@ -104,17 +116,20 @@ const POOL_0_POKEMON = [ SpeciesId.SOLROCK, SpeciesId.BALTOY, SpeciesId.CLAYDOL, + SpeciesId.CASTFORM, // ALL (Normal, Sunny, Rainy, Snowy) SpeciesId.DUSKULL, SpeciesId.TROPIUS, SpeciesId.CHIMECHO, - SpeciesId.GLALIE, // ? + SpeciesId.GLALIE, SpeciesId.SALAMENCE, + SpeciesId.BELDUM, SpeciesId.METANG, - SpeciesId.METAGROSS, // ? SpeciesId.LATIAS, SpeciesId.LATIOS, SpeciesId.RAYQUAZA, SpeciesId.JIRACHI, + SpeciesId.DEOXYS, // ALL (Normal, Attack, Defense, Speed) + SpeciesId.STARLY, SpeciesId.STARAVIA, SpeciesId.STARAPTOR, SpeciesId.MOTHIM, @@ -127,20 +142,28 @@ const POOL_0_POKEMON = [ SpeciesId.CHINGLING, SpeciesId.BRONZOR, SpeciesId.BRONZONG, + SpeciesId.CHATOT, SpeciesId.CARNIVINE, SpeciesId.MANTYKE, - SpeciesId.MAGNEZONE, // ? + SpeciesId.MAGNEZONE, SpeciesId.TOGEKISS, SpeciesId.YANMEGA, SpeciesId.GLISCOR, - SpeciesId.DUSKNOIR, // ? - SpeciesId.ROTOM, + SpeciesId.PORYGON_Z, + SpeciesId.PROBOPASS, + SpeciesId.DUSKNOIR, + SpeciesId.FROSLASS, + SpeciesId.ROTOM, // ALL (Normal, Heat, Wash, Frost, Fan, Mow) SpeciesId.UXIE, SpeciesId.MESPRIT, SpeciesId.AZELF, + SpeciesId.DIALGA, + SpeciesId.PALKIA, SpeciesId.GIRATINA, SpeciesId.CRESSELIA, - SpeciesId.ARCEUS, + SpeciesId.DARKRAI, + SpeciesId.ARCEUS, // ALL (Normal, Fighting, Flying, Poison, Ground, Rock, Bug, Ghost, Steel, Fire, Water, Grass, Electric, Psychic, Ice, Dragon, Dark, Fairy) + SpeciesId.PIDOVE, SpeciesId.TRANQUILL, SpeciesId.UNFEZANT, SpeciesId.WOOBAT, @@ -150,16 +173,24 @@ const POOL_0_POKEMON = [ SpeciesId.SOLOSIS, SpeciesId.DUOSION, SpeciesId.REUNICLUS, + SpeciesId.DUCKLETT, SpeciesId.SWANNA, + SpeciesId.VANILLITE, SpeciesId.VANILLISH, SpeciesId.VANILLUXE, SpeciesId.EMOLGA, + SpeciesId.KLINK, + SpeciesId.KLANG, + SpeciesId.KLINKLANG, SpeciesId.TYNAMO, SpeciesId.EELEKTRIK, SpeciesId.EELEKTROSS, + SpeciesId.ELGYEM, + SpeciesId.BEHEEYEM, SpeciesId.LAMPENT, SpeciesId.CHANDELURE, SpeciesId.CRYOGONAL, + SpeciesId.RUFFLET, SpeciesId.BRAVIARY, SpeciesId.MANDIBUZZ, SpeciesId.HYDREIGON, @@ -169,56 +200,96 @@ const POOL_0_POKEMON = [ SpeciesId.RESHIRAM, SpeciesId.ZEKROM, SpeciesId.LANDORUS, + SpeciesId.FLETCHLING, SpeciesId.FLETCHINDER, SpeciesId.TALONFLAME, - SpeciesId.VIVILLON, - SpeciesId.FLOETTE, - SpeciesId.FLORGES, - SpeciesId.HAWLUCHA, // ? + SpeciesId.VIVILLON, // ALL (Meadow, Icy Snow, Polar, Tundra, Continental, Garden, Elegant, Modern, Marine, Archipelago, High Plains, Sandstorm, River, Monsoon, Savanna, Sun, Ocean, Jungle, Fancy, Poke Ball) + SpeciesId.HAWLUCHA, + SpeciesId.KLEFKI, SpeciesId.NOIBAT, SpeciesId.NOIVERN, SpeciesId.YVELTAL, + SpeciesId.HOOPA, + SpeciesId.ROWLET, SpeciesId.DARTRIX, - SpeciesId.DECIDUEYE, //? + SpeciesId.PIKIPEK, SpeciesId.TRUMBEAK, SpeciesId.TOUCANNON, SpeciesId.VIKAVOLT, - SpeciesId.ORICORIO, + SpeciesId.ORICORIO, // ALL (Baile, Pompom, Pau, Sensu) + SpeciesId.CUTIEFLY, SpeciesId.RIBOMBEE, - SpeciesId.COMFEY, //? - SpeciesId.MINIOR, + SpeciesId.COMFEY, + SpeciesId.MINIOR, // ALL (Red, Orange, Yellow, Green, Blue, Indigo, Violet and Meteors) SpeciesId.TAPU_KOKO, SpeciesId.TAPU_LELE, SpeciesId.TAPU_BULU, SpeciesId.TAPU_FINI, + SpeciesId.COSMOG, + SpeciesId.COSMOEM, + SpeciesId.SOLGALEO, SpeciesId.LUNALA, SpeciesId.NIHILEGO, SpeciesId.BUZZWOLE, SpeciesId.CELESTEELA, - SpeciesId.NECROZMA, + SpeciesId.KARTANA, + SpeciesId.NECROZMA, // ALL (Dusk Mane, Dawn Wings, Ultra) SpeciesId.POIPOLE, SpeciesId.NAGANADEL, + SpeciesId.ROOKIDEE, SpeciesId.CORVISQUIRE, SpeciesId.CORVIKNIGHT, SpeciesId.ORBEETLE, SpeciesId.FLAPPLE, SpeciesId.CRAMORANT, + SpeciesId.SINISTEA, + SpeciesId.POLTEAGEIST, SpeciesId.FROSMOTH, + SpeciesId.DREEPY, SpeciesId.DRAKLOAK, SpeciesId.DRAGAPULT, SpeciesId.ETERNATUS, + SpeciesId.REGIELEKI, + SpeciesId.REGIDRAGO, + SpeciesId.CALYREX, SpeciesId.ENAMORUS, - SpeciesId.SQUAWKABILLY, + SpeciesId.SQUAWKABILLY, // ALL (Green, Blue, Yellow, White) SpeciesId.WATTREL, SpeciesId.KILOWATTREL, + SpeciesId.RABSCA, SpeciesId.BOMBIRDIER, + SpeciesId.VAROOM, + SpeciesId.REVAVROOM, + SpeciesId.GLIMMET, + SpeciesId.GLIMMORA, SpeciesId.FLAMIGO, + SpeciesId.SCREAM_TAIL, SpeciesId.FLUTTER_MANE, SpeciesId.IRON_JUGULIS, + SpeciesId.IRON_MOTH, + SpeciesId.CHI_YU, SpeciesId.ROARING_MOON, SpeciesId.MIRAIDON, - SpeciesId.KORAIDON, + SpeciesId.POLTCHAGEIST, + SpeciesId.SINISTCHA, + SpeciesId.FEZANDIPITI, + SpeciesId.PECHARUNT, + SpeciesId.ALOLA_RAICHU, + SpeciesId.GALAR_WEEZING, + SpeciesId.GALAR_ARTICUNO, + SpeciesId.GALAR_MOLTRES, + SpeciesId.HISUI_BRAVIARY, ]; +const POOL_BASEFORM = [SpeciesId.CHARIZARD]; +const POOL_ARIA = [ + SpeciesId.MELOETTA, // ARIA +]; +const POOL_MEGA = [SpeciesId.ALAKAZAM, SpeciesId.PINSIR, SpeciesId.METAGROSS]; +const POOL_MEGA_X = [SpeciesId.CHARIZARD]; +const POOL_MEGA_Y = [SpeciesId.CHARIZARD]; +const POOL_SKY = [SpeciesId.SHAYMIN]; +const POOL_BLACK = [SpeciesId.KYUREM]; +const POOL_WHITE = [SpeciesId.KYUREM]; const PHYSICAL_TUTOR_MOVES = [ MoveId.FLY, @@ -277,6 +348,42 @@ const sky_battle_requirements = new AnyCombinationPokemonRequirement( 3, new TypeRequirement(PokemonType.FLYING, false, 1), new AbilityRequirement(AbilityId.LEVITATE, false, 1), + new SpeciesRequirement(POOL_ALL_FORMS, 1, false), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_BASEFORM, 1, false), + CombinationPokemonRequirement.Some( + new FormPokemonRequirement("", 1), + new FormPokemonRequirement(SpeciesFormKey.NORMAL, 1), + ), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_MEGA, 1, false), + new FormPokemonRequirement(SpeciesFormKey.MEGA, 1), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_MEGA_X, 1, false), + new FormPokemonRequirement(SpeciesFormKey.MEGA_X, 1), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_MEGA_Y, 1, false), + new FormPokemonRequirement(SpeciesFormKey.MEGA_Y, 1), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_SKY, 1, false), + new FormPokemonRequirement(SpeciesFormKey.SKY, 1), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_BLACK, 1, false), + new FormPokemonRequirement(SpeciesFormKey.BLACK, 1), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_WHITE, 1, false), + new FormPokemonRequirement(SpeciesFormKey.WHITE, 1), + ), + CombinationPokemonRequirement.Every( + new SpeciesRequirement(POOL_ARIA, 1, false), + new FormPokemonRequirement(SpeciesFormKey.ARIA, 1), + ), ); // Helpful variables @@ -323,27 +430,26 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with { spriteKey: spriteKey, fileRoot: "trainer", - hasShadow: true, + hasShadow: false, x: 4, y: 7, - yShadow: 7, }, ]; const intro = [ { - text: `${namespace}:intro` + female ? "_f" : "", + text: female ? `${namespace}:intro_f` : `${namespace}:intro`, }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue` + female ? "_f" : "", + text: female ? `${namespace}:intro_dialogue_f` : `${namespace}:intro_dialogue`, }, ]; - const title = `${namespace}:title` + female ? "_f" : ""; - const description = `${namespace}:description` + female ? "_f" : ""; + const title = female ? `${namespace}:title_f` : `${namespace}:title`; + const description = female ? `${namespace}:description_f` : `${namespace}:description`; const outro = [ { - text: `${namespace}:outro` + female ? "_f" : "", + text: female ? `${namespace}:outro_f` : `${namespace}:outro`, }, ]; @@ -496,7 +602,7 @@ function getTrainerConfig(party_size: number, female: boolean): TrainerConfig { const name = female ? "sky_trainer_f" : "sky_trainer_m"; config.name = i18next.t("trainerNames:" + name); - let pool0Copy = POOL_0_POKEMON.slice(0); + let pool0Copy = POOL_ALL_FORMS.slice(0); pool0Copy = randSeedShuffle(pool0Copy); let pool0Mon = pool0Copy.pop()!; @@ -516,7 +622,10 @@ function doFlyingTypeTutor(): Promise { return new Promise(async resolve => { const moveOptions = globalScene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; const female = globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0].female; //TODO: Is this [0] correct enought? - await showEncounterDialogue(`${namespace}:battle_won` + female ? "_f" : "", `${namespace}:speaker`); + await showEncounterDialogue( + female ? `${namespace}:battle_won_f` : `${namespace}:battle_won`, + `${namespace}:speaker`, + ); const overlayScale = 1; const moveInfoOverlay = new MoveInfoOverlay({ diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 71ef7c60906..b69ef7efa41 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1195,3 +1195,44 @@ export class AnyCombinationPokemonRequirement extends EncounterPokemonRequiremen return this.requirements[0].getDialogueToken(pokemon); } } + +/** + * Find out if Pokemon in the party are of a specific form. + */ +export class FormPokemonRequirement extends EncounterPokemonRequirement { + private form: string; + + constructor(form: string, minNumberOfPokemon: number) { + super(); + this.invertQuery = false; + this.minNumberOfPokemon = minNumberOfPokemon; + this.form = form; + } + + /** + * Checks if at least {@linkcode minNumberOfPokemon} pokemon are of the specified form + * @returns true if at least {@linkcode minNumberOfPokemon} pokemon are of the specified form + */ + override meetsRequirement(): boolean { + const party = globalScene.getPlayerParty(); + return this.queryParty(party).length >= this.minNumberOfPokemon; + } + + /** + * Queries the players party for all party members that are of the specified form + * @param partyPokemon The party of {@linkcode PlayerPokemon} + * @returns All party members that are of the specified form + */ + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + return partyPokemon.filter(pokemon => pokemon.getFormKey() === this.form); + } + + /** + * Retrieves a dialogue token key/value pair for the given form. + * @param pokemon The {@linkcode PlayerPokemon} to check against + * @returns A dialogue token key/value pair + */ + override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { + return ["form", pokemon?.getFormKey() ?? ""]; + } +} diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 81d94bb5ace..50457bea919 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -6046,7 +6046,7 @@ export const trainerConfigs: TrainerConfigs = { .setLocalizedName("Future Self F") .setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)), [TrainerType.SKY_TRAINER]: new TrainerConfig(++t) - .setHasGenders("Sky Trainer Felicia") + .setHasGenders("Sky Trainer") .setMoneyMultiplier(2.25) .setEncounterBgm(TrainerType.ACE_TRAINER) .setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)), diff --git a/src/enums/species-form-key.ts b/src/enums/species-form-key.ts index b324c876b87..d7bb30c636d 100644 --- a/src/enums/species-form-key.ts +++ b/src/enums/species-form-key.ts @@ -1,4 +1,5 @@ export enum SpeciesFormKey { + NORMAL = "normal", MEGA = "mega", MEGA_X = "mega-x", MEGA_Y = "mega-y", @@ -10,4 +11,8 @@ export enum SpeciesFormKey { GIGANTAMAX_SINGLE = "gigantamax-single", GIGANTAMAX_RAPID = "gigantamax-rapid", ETERNAMAX = "eternamax", + SKY = "sky", + BLACK = "black", + WHITE = "white", + ARIA = "aria", } From 3e560c4a14695c5d4738882c0ba746502120b38a Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 21 Jun 2025 15:56:06 -0700 Subject: [PATCH 6/7] Update mystery-encounters.ts --- src/data/mystery-encounters/mystery-encounters.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 8064fb5de35..c63b75f5725 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -33,7 +33,8 @@ import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/ 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 { getBiomeName } from "#app/data/balance/biomes"; -import { SkyBattleEncounter } from "./encounters/sky-battle-encounter"; +// Temporarily disabled pending ME rebalancing +// import { SkyBattleEncounter } from "./encounters/sky-battle-encounter"; export const EXTREME_ENCOUNTER_BIOMES = [ BiomeId.SEA, From 7cd28dc959a7d2acee148e6d8ef509081f27bef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Marques?= Date: Thu, 26 Jun 2025 19:06:45 +0100 Subject: [PATCH 7/7] Created enemy pools Changed the way moves are disabled Added more female keys for locales Co-authored-by: Diogo Diniz --- .../encounters/sky-battle-encounter.ts | 1042 +++++++++++++++-- 1 file changed, 976 insertions(+), 66 deletions(-) diff --git a/src/data/mystery-encounters/encounters/sky-battle-encounter.ts b/src/data/mystery-encounters/encounters/sky-battle-encounter.ts index 2d5dc798a47..630d4e022d3 100644 --- a/src/data/mystery-encounters/encounters/sky-battle-encounter.ts +++ b/src/data/mystery-encounters/encounters/sky-battle-encounter.ts @@ -25,7 +25,7 @@ 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 { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils/common"; import type { PlayerPokemon } from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import i18next from "i18next"; @@ -291,6 +291,883 @@ const POOL_SKY = [SpeciesId.SHAYMIN]; const POOL_BLACK = [SpeciesId.KYUREM]; const POOL_WHITE = [SpeciesId.KYUREM]; +// trainer pools +const WEAK_TIER = [ + SpeciesId.PIDGEY, + SpeciesId.SPEAROW, + SpeciesId.ZUBAT, + SpeciesId.MAGNEMITE, + SpeciesId.GASTLY, + SpeciesId.KOFFING, + SpeciesId.PORYGON, + SpeciesId.HOOTHOOT, + SpeciesId.LEDYBA, + SpeciesId.NATU, + SpeciesId.HOPPIP, + SpeciesId.SKIPLOOM, + SpeciesId.YANMA, + SpeciesId.MURKROW, + SpeciesId.MISDREAVUS, + SpeciesId.UNOWN, + SpeciesId.GLIGAR, + SpeciesId.DELIBIRD, + SpeciesId.TAILLOW, + SpeciesId.WINGULL, + SpeciesId.SHEDINJA, + SpeciesId.VOLBEAT, + SpeciesId.ILLUMISE, + SpeciesId.SWABLU, + SpeciesId.BALTOY, + SpeciesId.CASTFORM, + SpeciesId.DUSKULL, + SpeciesId.BELDUM, + SpeciesId.STARLY, + SpeciesId.COMBEE, + SpeciesId.DRIFLOON, + SpeciesId.CHINGLING, + SpeciesId.BRONZOR, + SpeciesId.CHATOT, + SpeciesId.MANTYKE, + SpeciesId.PIDOVE, + SpeciesId.WOOBAT, + SpeciesId.SOLOSIS, + SpeciesId.DUCKLETT, + SpeciesId.VANILLITE, + SpeciesId.EMOLGA, + SpeciesId.KLINK, + SpeciesId.TYNAMO, + SpeciesId.ELGYEM, + SpeciesId.RUFFLET, + SpeciesId.FLETCHLING, + SpeciesId.KLEFKI, + SpeciesId.NOIBAT, + SpeciesId.ROWLET, + SpeciesId.PIKIPEK, + SpeciesId.CUTIEFLY, + SpeciesId.COMFEY, + SpeciesId.MINIOR, + SpeciesId.COSMOG, + SpeciesId.POIPOLE, + SpeciesId.ROOKIDEE, + SpeciesId.SINISTEA, + SpeciesId.DREEPY, + SpeciesId.SQUAWKABILLY, + SpeciesId.WATTREL, + SpeciesId.VAROOM, + SpeciesId.GLIMMET, + SpeciesId.POLTCHAGEIST, +]; + +const MID_TIER = [ + { + species: SpeciesId.BUTTERFREE, + formIndex: 0, + }, + { + species: SpeciesId.BEEDRILL, + formIndex: 0, + }, + { + species: SpeciesId.PIDGEOTTO, + formIndex: 0, + }, + { + species: SpeciesId.PIDGEOT, + formIndex: 0, + }, + { + species: SpeciesId.FEAROW, + formIndex: 0, + }, + { + species: SpeciesId.GOLBAT, + formIndex: 0, + }, + { + species: SpeciesId.VENOMOTH, + formIndex: 0, + }, + { + species: SpeciesId.MAGNETON, + formIndex: 0, + }, + { + species: SpeciesId.HAUNTER, + formIndex: 0, + }, + { + species: SpeciesId.WEEZING, + formIndex: 0, + }, + { + species: SpeciesId.SCYTHER, + formIndex: 0, + }, + { + species: SpeciesId.NOCTOWL, + formIndex: 0, + }, + { + species: SpeciesId.LEDIAN, + formIndex: 0, + }, + { + species: SpeciesId.CROBAT, + formIndex: 0, + }, + { + species: SpeciesId.TOGETIC, + formIndex: 0, + }, + { + species: SpeciesId.XATU, + formIndex: 0, + }, + { + species: SpeciesId.JUMPLUFF, + formIndex: 0, + }, + { + species: SpeciesId.MANTINE, + formIndex: 0, + }, + { + species: SpeciesId.SKARMORY, + formIndex: 0, + }, + { + species: SpeciesId.PORYGON2, + formIndex: 0, + }, + { + species: SpeciesId.BEAUTIFLY, + formIndex: 0, + }, + { + species: SpeciesId.DUSTOX, + formIndex: 0, + }, + { + species: SpeciesId.SWELLOW, + formIndex: 0, + }, + { + species: SpeciesId.PELIPPER, + formIndex: 0, + }, + { + species: SpeciesId.MASQUERAIN, + formIndex: 0, + }, + { + species: SpeciesId.VIBRAVA, + formIndex: 0, + }, + { + species: SpeciesId.ALTARIA, + formIndex: 0, + }, + { + species: SpeciesId.LUNATONE, + formIndex: 0, + }, + { + species: SpeciesId.SOLROCK, + formIndex: 0, + }, + { + species: SpeciesId.CLAYDOL, + formIndex: 0, + }, + { + species: SpeciesId.TROPIUS, + formIndex: 0, + }, + { + species: SpeciesId.CHIMECHO, + formIndex: 0, + }, + { + species: SpeciesId.GLALIE, + formIndex: 0, + }, + { + species: SpeciesId.METANG, + formIndex: 0, + }, + { + species: SpeciesId.STARAVIA, + formIndex: 0, + }, + { + species: SpeciesId.STARAPTOR, + formIndex: 0, + }, + { + species: SpeciesId.MOTHIM, + formIndex: 0, + }, + { + species: SpeciesId.VESPIQUEN, + formIndex: 0, + }, + { + species: SpeciesId.DRIFBLIM, + formIndex: 0, + }, + { + species: SpeciesId.MISMAGIUS, + formIndex: 0, + }, + { + species: SpeciesId.HONCHKROW, + formIndex: 0, + }, + { + species: SpeciesId.BRONZONG, + formIndex: 0, + }, + { + species: SpeciesId.CARNIVINE, + formIndex: 0, + }, + { + species: SpeciesId.MAGNEZONE, + formIndex: 0, + }, + { + species: SpeciesId.TOGEKISS, + formIndex: 0, + }, + { + species: SpeciesId.YANMEGA, + formIndex: 0, + }, + { + species: SpeciesId.GLISCOR, + formIndex: 0, + }, + { + species: SpeciesId.PORYGON_Z, + formIndex: 0, + }, + { + species: SpeciesId.PROBOPASS, + formIndex: 0, + }, + { + species: SpeciesId.DUSKNOIR, + formIndex: 0, + }, + { + species: SpeciesId.FROSLASS, + formIndex: 0, + }, + { + species: SpeciesId.ROTOM, // allow all forms + formIndex: 0, // normal + }, + { + species: SpeciesId.ROTOM, + formIndex: 1, // heat + }, + { + species: SpeciesId.ROTOM, + formIndex: 2, // wash + }, + { + species: SpeciesId.ROTOM, + formIndex: 3, // frost + }, + { + species: SpeciesId.ROTOM, + formIndex: 4, // fan + }, + { + species: SpeciesId.ROTOM, + formIndex: 5, // mow + }, + { + species: SpeciesId.TRANQUILL, + formIndex: 0, + }, + { + species: SpeciesId.UNFEZANT, + formIndex: 0, + }, + { + species: SpeciesId.SWOOBAT, + formIndex: 0, + }, + { + species: SpeciesId.SIGILYPH, + formIndex: 0, + }, + { + species: SpeciesId.ARCHEOPS, + formIndex: 0, + }, + { + species: SpeciesId.DUOSION, + formIndex: 0, + }, + { + species: SpeciesId.REUNICLUS, + formIndex: 0, + }, + { + species: SpeciesId.SWANNA, + formIndex: 0, + }, + { + species: SpeciesId.VANILLISH, + formIndex: 0, + }, + { + species: SpeciesId.VANILLUXE, + formIndex: 0, + }, + { + species: SpeciesId.KLANG, + formIndex: 0, + }, + { + species: SpeciesId.KLINKLANG, + formIndex: 0, + }, + { + species: SpeciesId.EELEKTRIK, + formIndex: 0, + }, + { + species: SpeciesId.EELEKTROSS, + formIndex: 0, + }, + { + species: SpeciesId.BEHEEYEM, + formIndex: 0, + }, + { + species: SpeciesId.LAMPENT, + formIndex: 0, + }, + { + species: SpeciesId.CHANDELURE, + formIndex: 0, + }, + { + species: SpeciesId.CRYOGONAL, + formIndex: 0, + }, + { + species: SpeciesId.BRAVIARY, + formIndex: 0, + }, + { + species: SpeciesId.MANDIBUZZ, + formIndex: 0, + }, + { + species: SpeciesId.MELOETTA, + formIndex: 0, + }, // Aria + { + species: SpeciesId.FLETCHINDER, + formIndex: 0, + }, + { + species: SpeciesId.TALONFLAME, + formIndex: 0, + }, + { + species: SpeciesId.VIVILLON, + formIndex: 0, + }, + { + species: SpeciesId.HAWLUCHA, + formIndex: 0, + }, + { + species: SpeciesId.DARTRIX, + formIndex: 0, + }, + { + species: SpeciesId.TRUMBEAK, + formIndex: 0, + }, + { + species: SpeciesId.TOUCANNON, + formIndex: 0, + }, + { + species: SpeciesId.VIKAVOLT, + formIndex: 0, + }, + { + species: SpeciesId.ORICORIO, // ALL (Baile, Pompom, Pau, Sensu) + formIndex: 0, // Baile + }, + { + species: SpeciesId.ORICORIO, + formIndex: 1, // Pompom + }, + { + species: SpeciesId.ORICORIO, + formIndex: 2, // Pau + }, + { + species: SpeciesId.ORICORIO, + formIndex: 3, // Sensu + }, + { + species: SpeciesId.RIBOMBEE, + formIndex: 0, + }, + { + species: SpeciesId.COSMOEM, + formIndex: 0, + }, + { + species: SpeciesId.CORVISQUIRE, + formIndex: 0, + }, + { + species: SpeciesId.CORVIKNIGHT, + formIndex: 0, + }, + { + species: SpeciesId.ORBEETLE, + formIndex: 0, + }, + { + species: SpeciesId.FLAPPLE, + formIndex: 0, + }, + { + species: SpeciesId.CRAMORANT, + formIndex: 0, + }, + { + species: SpeciesId.POLTEAGEIST, + formIndex: 0, + }, + { + species: SpeciesId.FROSMOTH, + formIndex: 0, + }, + { + species: SpeciesId.DRAKLOAK, + formIndex: 0, + }, + { + species: SpeciesId.RABSCA, + formIndex: 0, + }, + { + species: SpeciesId.BOMBIRDIER, + formIndex: 0, + }, + { + species: SpeciesId.REVAVROOM, + formIndex: 0, + }, + { + species: SpeciesId.FLAMIGO, + formIndex: 0, + }, + { + species: SpeciesId.SINISTCHA, + formIndex: 0, + }, +]; + +const STRONG_TIER: { species: SpeciesId; formIndex?: number }[] = [ + { + species: SpeciesId.CHARIZARD, + formIndex: 0, + }, + { + species: SpeciesId.CHARIZARD, + formIndex: 1, + }, + { + species: SpeciesId.CHARIZARD, + formIndex: 2, + }, + { + species: SpeciesId.BUTTERFREE, + formIndex: 1, + }, + { + species: SpeciesId.PIDGEOT, + formIndex: 1, + }, + { + species: SpeciesId.ALAKAZAM, + formIndex: 1, + }, + { + species: SpeciesId.GENGAR, + formIndex: 0, + }, + { + species: SpeciesId.GYARADOS, + formIndex: 0, + }, + { + species: SpeciesId.GYARADOS, + formIndex: 1, + }, + { + species: SpeciesId.AERODACTYL, + formIndex: 0, + }, + { + species: SpeciesId.AERODACTYL, + formIndex: 1, + }, + { + species: SpeciesId.ARTICUNO, + }, + { + species: SpeciesId.ZAPDOS, + }, + { + species: SpeciesId.MOLTRES, + }, + { + species: SpeciesId.DRAGONITE, + }, + { + species: SpeciesId.MEWTWO, + formIndex: 0, + }, + { + species: SpeciesId.MEWTWO, + formIndex: 1, + }, + { + species: SpeciesId.MEWTWO, + formIndex: 2, + }, + { + species: SpeciesId.MEW, + }, + { + species: SpeciesId.LUGIA, + }, + { + species: SpeciesId.HO_OH, + }, + { + species: SpeciesId.CELEBI, + }, + { + species: SpeciesId.FLYGON, + }, + { + species: SpeciesId.ALTARIA, + formIndex: 1, + }, + { + species: SpeciesId.GLALIE, + formIndex: 1, + }, + { + species: SpeciesId.SALAMENCE, + formIndex: 0, + }, + { + species: SpeciesId.SALAMENCE, + formIndex: 1, + }, + { + species: SpeciesId.METAGROSS, + formIndex: 1, + }, + { + species: SpeciesId.LATIAS, + formIndex: 0, + }, + { + species: SpeciesId.LATIAS, + formIndex: 1, + }, + { + species: SpeciesId.LATIOS, + formIndex: 0, + }, + { + species: SpeciesId.LATIOS, + formIndex: 1, + }, + { + species: SpeciesId.RAYQUAZA, + formIndex: 0, + }, + { + species: SpeciesId.RAYQUAZA, + formIndex: 1, + }, + { + species: SpeciesId.JIRACHI, + }, + { + species: SpeciesId.DEOXYS, + formIndex: 0, + }, + { + species: SpeciesId.DEOXYS, + formIndex: 1, + }, + { + species: SpeciesId.DEOXYS, + formIndex: 2, + }, + { + species: SpeciesId.DEOXYS, + formIndex: 3, + }, + { + species: SpeciesId.UXIE, + }, + { + species: SpeciesId.MESPRIT, + }, + { + species: SpeciesId.AZELF, + }, + { + species: SpeciesId.DIALGA, + formIndex: 0, + }, + { + species: SpeciesId.DIALGA, + formIndex: 1, + }, + { + species: SpeciesId.PALKIA, + formIndex: 0, + }, + { + species: SpeciesId.PALKIA, + formIndex: 1, + }, + { + species: SpeciesId.GIRATINA, + formIndex: 0, + }, + { + species: SpeciesId.GIRATINA, + formIndex: 1, + }, + { + species: SpeciesId.CRESSELIA, + }, + { + species: SpeciesId.DARKRAI, + }, + { + species: SpeciesId.SHAYMIN, + formIndex: 1, + }, + { + species: SpeciesId.ARCEUS, + }, + { + species: SpeciesId.HYDREIGON, + }, + { + species: SpeciesId.VOLCARONA, + }, + { + species: SpeciesId.TORNADUS, + formIndex: 0, + }, + { + species: SpeciesId.TORNADUS, + formIndex: 1, + }, + { + species: SpeciesId.THUNDURUS, + formIndex: 0, + }, + { + species: SpeciesId.THUNDURUS, + formIndex: 1, + }, + { + species: SpeciesId.RESHIRAM, + }, + { + species: SpeciesId.ZEKROM, + }, + { + species: SpeciesId.KYUREM, + formIndex: 1, + }, + { + species: SpeciesId.KYUREM, + formIndex: 2, + }, + { + species: SpeciesId.LANDORUS, + formIndex: 0, + }, + { + species: SpeciesId.LANDORUS, + formIndex: 1, + }, + { + species: SpeciesId.NOIVERN, + }, + { + species: SpeciesId.YVELTAL, + }, + { + species: SpeciesId.HOOPA, + formIndex: 0, + }, + { + species: SpeciesId.HOOPA, + formIndex: 1, + }, + { + species: SpeciesId.TAPU_KOKO, + }, + { + species: SpeciesId.TAPU_LELE, + }, + { + species: SpeciesId.TAPU_BULU, + }, + { + species: SpeciesId.TAPU_FINI, + }, + { + species: SpeciesId.SOLGALEO, + }, + { + species: SpeciesId.LUNALA, + }, + { + species: SpeciesId.NIHILEGO, + }, + { + species: SpeciesId.BUZZWOLE, + }, + { + species: SpeciesId.CELESTEELA, + }, + { + species: SpeciesId.KARTANA, + }, + { + species: SpeciesId.NECROZMA, + formIndex: 0, + }, + { + species: SpeciesId.NECROZMA, + formIndex: 1, + }, + { + species: SpeciesId.NECROZMA, + formIndex: 2, + }, + { + species: SpeciesId.NECROZMA, + formIndex: 3, + }, + { + species: SpeciesId.NAGANADEL, + }, + { + species: SpeciesId.CORVIKNIGHT, + formIndex: 1, + }, + { + species: SpeciesId.ORBEETLE, + formIndex: 1, + }, + { + species: SpeciesId.DRAGAPULT, + }, + { + species: SpeciesId.ETERNATUS, + formIndex: 0, + }, + { + species: SpeciesId.ETERNATUS, + formIndex: 1, + }, + { + species: SpeciesId.REGIELEKI, + }, + { + species: SpeciesId.REGIDRAGO, + }, + { + species: SpeciesId.CALYREX, + formIndex: 0, + }, + { + species: SpeciesId.ENAMORUS, + formIndex: 0, + }, + { + species: SpeciesId.ENAMORUS, + formIndex: 1, + }, + { + species: SpeciesId.KILOWATTREL, + }, + { + species: SpeciesId.GLIMMORA, + }, + { + species: SpeciesId.SCREAM_TAIL, + }, + { + species: SpeciesId.FLUTTER_MANE, + }, + { + species: SpeciesId.IRON_JUGULIS, + }, + { + species: SpeciesId.IRON_MOTH, + }, + { + species: SpeciesId.CHI_YU, + }, + { + species: SpeciesId.ROARING_MOON, + }, + { + species: SpeciesId.MIRAIDON, + }, + { + species: SpeciesId.FEZANDIPITI, + }, + { + species: SpeciesId.PECHARUNT, + }, + { + species: SpeciesId.ALOLA_RAICHU, + }, + { + species: SpeciesId.GALAR_WEEZING, + }, + { + species: SpeciesId.GALAR_ARTICUNO, + }, + { + species: SpeciesId.GALAR_MOLTRES, + }, + { + species: SpeciesId.HISUI_BRAVIARY, + }, +]; + const PHYSICAL_TUTOR_MOVES = [ MoveId.FLY, MoveId.BRAVE_BIRD, @@ -305,11 +1182,11 @@ const SPECIAL_TUTOR_MOVES = [MoveId.AEROBLAST, MoveId.AIR_SLASH, MoveId.HURRICAN 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.SAND_ATTACK, // extra move left here to test code with pidgey (with no need for overrides) MoveId.DIVE, MoveId.EARTH_POWER, MoveId.EARTHQUAKE, @@ -386,9 +1263,10 @@ const sky_battle_requirements = new AnyCombinationPokemonRequirement( ), ); +const WAVE_LEVEL_BREAKPOINTS = [80, 150]; + // Helpful variables -let originalUsedPP: number[] = []; -const disallowedPokemon: Map = new Map(); +let female = false; /** * Sky Battle encounter. @@ -417,8 +1295,8 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with const partySize: number = sky_battle_requirements.queryParty(globalScene.getPlayerParty()).length; // randomize trainer gender - const female = !!randSeedInt(2); - const config = getTrainerConfig(partySize, female); + female = !!randSeedInt(2); + const config = getTrainerConfig(partySize, globalScene.currentBattle.waveIndex); const spriteKey = config.getSpriteKey(female); encounter.enemyPartyConfigs.push({ trainerConfig: config, @@ -445,7 +1323,7 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with text: female ? `${namespace}:intro_dialogue_f` : `${namespace}:intro_dialogue`, }, ]; - const title = female ? `${namespace}:title_f` : `${namespace}:title`; + const title = `${namespace}:title`; const description = female ? `${namespace}:description_f` : `${namespace}:description`; const outro = [ { @@ -486,61 +1364,42 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with selected: [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.1.selected`, + text: female ? `${namespace}:option.1.selected_f` : `${namespace}:option.1.selected`, }, ], }, async () => { + // TODO: Update selected text based on female + // 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)])); + moveTutorOptions.push( + new PokemonMove(PHYSICAL_TUTOR_MOVES[randSeedInt(PHYSICAL_TUTOR_MOVES.length)]), + new PokemonMove(SPECIAL_TUTOR_MOVES[randSeedInt(SPECIAL_TUTOR_MOVES.length)]), + 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(); - }), - ); + //Ordering here is relevant + disableDisallowedPokemon(); + disableIllegalMoves(); // Assigns callback that teaches move before continuing to rewards encounter.onRewards = doFlyingTypeTutor; setEncounterRewards({ fillRemaining: true }); + encounter.spriteConfigs[0].hasShadow = false; //TODO: Is this [0] correct enough? 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(); - }), - ); + disableEnemyIllegalMoves(); }, ) .withOption( @@ -563,7 +1422,7 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with encounter.selectedOption!.dialogue!.selected = [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.2.selected`, + text: female ? `${namespace}:option.2.selected_f` : `${namespace}:option.2.selected`, }, ]; }) @@ -580,7 +1439,7 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with buttonTooltip: `${namespace}:option.3.tooltip`, selected: [ { - text: `${namespace}:option.3.selected`, + text: female ? `${namespace}:option.3.selected_f` : `${namespace}:option.3.selected`, }, ], }, @@ -596,22 +1455,39 @@ export const SkyBattleEncounter: MysteryEncounter = MysteryEncounterBuilder.with ]) .build(); -function getTrainerConfig(party_size: number, female: boolean): TrainerConfig { +function getTrainerConfig(party_size: number, wave_index: number): 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); + config.name = i18next.t("trainerNames:sky_trainer"); - let pool0Copy = POOL_ALL_FORMS.slice(0); - pool0Copy = randSeedShuffle(pool0Copy); - let pool0Mon = pool0Copy.pop()!; + // choose pool according to wave + let trainer_pool: any; + if (wave_index < WAVE_LEVEL_BREAKPOINTS[0]) { + trainer_pool = WEAK_TIER.slice(0); + } else if (wave_index < WAVE_LEVEL_BREAKPOINTS[1]) { + trainer_pool = MID_TIER.slice(0); + } else { + trainer_pool = STRONG_TIER.slice(0); + } + + trainer_pool = randSeedShuffle(trainer_pool); 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()!; + const rand_pokemon = trainer_pool.pop()!; + const rand_species = isNullOrUndefined(rand_pokemon.species) ? rand_pokemon : rand_pokemon.species; + config.setPartyMemberFunc( + index, + getRandomPartyMemberFunc([rand_species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(rand_pokemon.formIndex)) { + p.formIndex = rand_pokemon.formIndex; + p.generateAndPopulateMoveset(); + p.generateName(); + } + }), + ); } return config; @@ -621,7 +1497,11 @@ function doFlyingTypeTutor(): Promise { // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO explain return new Promise(async resolve => { const moveOptions = globalScene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; - const female = globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0].female; //TODO: Is this [0] correct enought? + + //Ordering here is relevant + reEnableIllegalMoves(); + reEnableDisallowedPokemon(); + await showEncounterDialogue( female ? `${namespace}:battle_won_f` : `${namespace}:battle_won`, `${namespace}:speaker`, @@ -677,22 +1557,52 @@ function doFlyingTypeTutor(): Promise { ); } - // 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(); }); } + +const disallowedPokemon: Map = new Map(); +function disableDisallowedPokemon(): void { + disallowedPokemon.clear(); + const allowedPokemon = sky_battle_requirements.queryParty(globalScene.getPlayerParty()); + globalScene.getPlayerParty().filter(pokemon => !allowedPokemon.includes(pokemon)); + globalScene.getPlayerParty().forEach((pokemon, index) => { + if (!allowedPokemon.includes(pokemon)) { + disallowedPokemon.set(index, pokemon); + } + }); + + disallowedPokemon.forEach(pokemon => globalScene.removePokemonFromPlayerParty(pokemon, false)); +} + +function reEnableDisallowedPokemon(): void { + disallowedPokemon.forEach((pokemon, index) => { + globalScene.getPlayerParty().splice(index, 0, pokemon); + }); +} + +let originalMovesets: PokemonMove[][] = []; +function disableIllegalMoves(): void { + originalMovesets = []; + globalScene.getPlayerParty().forEach(pokemon => { + originalMovesets.push(pokemon.moveset.slice(0)); + pokemon.moveset = pokemon.moveset.filter(move => !INELIGIBLE_MOVES.includes(move.getMove().id)); + }); +} + +function reEnableIllegalMoves(): void { + originalMovesets.forEach((moveset, idx) => { + globalScene.getPlayerParty()[idx].moveset = moveset; + }); +} + +function disableEnemyIllegalMoves(): void { + globalScene.getEnemyParty().forEach(pokemon => + pokemon.moveset + .filter(move => INELIGIBLE_MOVES.includes(move.getMove().id)) + .forEach(move => { + move.ppUsed = move.getMovePp(); + }), + ); +}