diff --git a/.github/workflows/mystery-events.yml b/.github/workflows/mystery-events.yml index 66ab3dd1659..621bffb3e2b 100644 --- a/.github/workflows/mystery-events.yml +++ b/.github/workflows/mystery-events.yml @@ -50,4 +50,4 @@ jobs: run: npm ci # Use 'npm ci' to install dependencies - name: tests # Step to run tests - run: npm run test${{ runner.debug == '0' &&':silent' || '' }} # silent on default. if debug run loud + run: npm run test${{ runner.debug == '0' &&':silent' || '' }} # silent on default. if debug run loud. diff --git a/public/images/mystery-encounters/berry_juice.json b/public/images/mystery-encounters/berry_juice.json new file mode 100644 index 00000000000..1a5331368ad --- /dev/null +++ b/public/images/mystery-encounters/berry_juice.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "berry_juice.png", + "format": "RGBA8888", + "size": { + "w": 24, + "h": 23 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 24, + "h": 24 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 22, + "h": 21 + }, + "frame": { + "x": 1, + "y": 1, + "w": 22, + "h": 21 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:04685a0eb6ef9095824b65408ec1b38f:9891674d538df100fcddde29330c21ae:927f117bdb1c2a27226a5540ce00ee8b$" + } +} diff --git a/public/images/mystery-encounters/berry_juice.png b/public/images/mystery-encounters/berry_juice.png new file mode 100644 index 00000000000..c0986b804f9 Binary files /dev/null and b/public/images/mystery-encounters/berry_juice.png differ diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 698b374497b..1aba405745a 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -4,7 +4,7 @@ import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "../../../battle-scene"; -import { AddPokeballModifierType } from "../../../modifier/modifier-type"; +import { AddPokeballModifierType } from "#app/modifier/modifier-type"; import { PokeballType } from "../../pokeball"; import { getPokemonSpecies } from "../../pokemon-species"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 8fad5e69a6c..d61a0c1ff4e 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -1,5 +1,5 @@ import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { applyDamageToPokemon, EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; @@ -17,6 +17,7 @@ import { WeatherType } from "#app/data/weather"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { StatusEffect } from "#app/data/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:fieryFallout"; diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 3418f977ccd..7becc65a160 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -5,7 +5,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { applyDamageToPokemon, leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; +import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; +import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; const OPTION_1_REQUIRED_MOVE = Moves.SURF; const OPTION_2_REQUIRED_MOVE = Moves.FLY; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 620d5380754..e8db89a6961 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -28,7 +28,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = hasShadow: true, x: 4, y: 10, - yShadowOffset: 3, + yShadow: 3, disableAnimation: true, // Re-enabled after option select }, ]) @@ -109,7 +109,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = scene, true ); - koPlayerPokemon(highestLevelPokemon); + koPlayerPokemon(scene, highestLevelPokemon); scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); // Show which Pokemon was KOed, then leave encounter with no rewards diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 28c60a0eb10..147ce2a5e2c 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -38,7 +38,7 @@ export const SafariZoneEncounter: IMysteryEncounter = hasShadow: true, x: 4, y: 10, - yShadowOffset: 3 + yShadow: 3 }, ]) .withIntroDialogue([ diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index de20a1e11b0..0325eb2f2a6 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -10,6 +10,7 @@ import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } fro import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoneyRequirement } from "../mystery-encounter-requirements"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:shadyVitaminDealer"; @@ -35,7 +36,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = repeat: true, x: 12, y: -5, - yShadowOffset: -5 + yShadow: -5 }, { spriteKey: "b2w2_veteran_m", @@ -43,7 +44,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = hasShadow: true, x: -12, y: 3, - yShadowOffset: 3 + yShadow: 3 }, ]) .withIntroDialogue([ @@ -122,8 +123,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = const chosenPokemon = encounter.misc.chosenPokemon; // Pokemon takes 1/3 max HP damage - const damage = Math.round(chosenPokemon.getMaxHp() / 3); - chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0); + applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 3)); // Roll for poison (80%) if (randSeedInt(10) < 8) { diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts new file mode 100644 index 00000000000..b199b43e952 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -0,0 +1,183 @@ +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Species } from "#enums/species"; +import { Nature } from "#app/data/nature"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { StatChangePhase } from "#app/phases"; +import { BattleStat } from "#app/data/battle-stat"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theStrongStuff"; + +export const TheStrongStuffEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_STRONG_STUFF) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "berry_juice", + fileRoot: "mystery-encounters", + hasShadow: true, + scale: 1.5, + x: -15, + y: 3, + yShadow: 0 + }, + { + spriteKey: Species.SHUCKLE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + scale: 1.5, + x: 20, + y: 10, + yShadow: 7 + }, + ]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}:intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SHUCKLE), + isBoss: true, + bossSegments: 5, + spriteScale: 1.5, + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierTypes: [ + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.APICOT]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.GANLON]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.LUM]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.LUM]).type as PokemonHeldItemModifierType + ], + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}:option:2:stat_boost`); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF, BattleStat.SPDEF], 2)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + initCustomMovesForEncounter(scene, [Moves.GASTRO_ACID, Moves.STEALTH_ROCK]); + + return true; + }) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) + .withSimpleOption( + { + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + selected: [ + { + text: `${namespace}:option:1:selected` + } + ] + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Do blackout and hide intro visuals during blackout + scene.time.delayedCall(750, () => { + transitionMysteryEncounterIntroVisuals(scene, true, true, 50); + }); + + // -20 to all base stats of highest BST, +10 to all base stats of rest of party + // Get highest BST mon + const party = scene.getParty(); + let highestBst: PlayerPokemon = null; + let statTotal = 0; + for (const pokemon of party) { + if (!highestBst) { + highestBst = pokemon; + statTotal = pokemon.getSpeciesForm().getBaseStatTotal(); + continue; + } + + const total = pokemon.getSpeciesForm().getBaseStatTotal(); + if (total > statTotal) { + highestBst = pokemon; + statTotal = total; + } + } + + if (!highestBst) { + highestBst = party[0]; + } + + modifyPlayerPokemonBST(highestBst, -20); + for (const pokemon of party) { + if (highestBst.id === pokemon.id) { + continue; + } + + modifyPlayerPokemonBST(pokemon, 10); + } + + encounter.setDialogueToken("highBstPokemon", highestBst.name); + await showEncounterText(scene, `${namespace}:option:1:selected_2`, null, true); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], fillRemaining: true }); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.GASTRO_ACID), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.STEALTH_ROCK), + ignorePp: true + }); + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 2800a0befd1..2c09b0a01c7 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -40,7 +40,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = hasShadow: true, y: 6, x: 5, - yShadowOffset: -2 + yShadow: -2 }, ]) .withIntroDialogue([ diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 9163c16357b..7679ab20d58 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -13,6 +13,7 @@ import { TrainingSessionEncounter } from "./encounters/training-session-encounte import IMysteryEncounter from "./mystery-encounter"; import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter"; import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; +import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -181,7 +182,9 @@ export const mysteryEncountersByBiome = new Map([ [Biome.SEABED, []], [Biome.MOUNTAIN, []], [Biome.BADLANDS, []], - [Biome.CAVE, []], + [Biome.CAVE, [ + MysteryEncounterType.THE_STRONG_STUFF + ]], [Biome.DESERT, []], [Biome.ICE_CAVE, []], [Biome.MEADOW, []], @@ -221,6 +224,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter; allMysteryEncounters[MysteryEncounterType.LOST_AT_SEA] = LostAtSeaEncounter; allMysteryEncounters[MysteryEncounterType.FIERY_FALLOUT] = FieryFalloutEncounter; + allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 4e2c0b3faa5..40763358b89 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -2,9 +2,8 @@ import { BattlerIndex, BattleType } from "#app/battle"; import { biomeLinks } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; -import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import * as Overrides from "#app/overrides"; @@ -27,6 +26,7 @@ import { Status, StatusEffect } from "../../status-effect"; import { TrainerConfig, trainerConfigs, TrainerSlot } from "../../trainer-config"; import { MysteryEncounterVariant } from "../mystery-encounter"; import { Gender } from "#app/data/gender"; +import { Nature } from "#app/data/nature"; import { Moves } from "#enums/moves"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; @@ -54,7 +54,7 @@ export function doTrainerExclamation(scene: BattleScene) { } }); - scene.playSound("GEN8- Exclaim.wav", { volume: 0.8 }); + scene.playSound("GEN8- Exclaim.wav", { volume: 0.7 }); } export interface EnemyPokemonConfig { @@ -62,11 +62,14 @@ export interface EnemyPokemonConfig { isBoss: boolean; bossSegments?: number; bossSegmentModifier?: number; // Additive to the determined segment number + spriteScale?: number; formIndex?: number; level?: number; gender?: Gender; passive?: boolean; moveSet?: Moves[]; + nature?: Nature; + ivs?: [integer, integer, integer, integer, integer, integer]; /** Can set just the status, or pass a timer on the status turns */ status?: StatusEffect | [StatusEffect, number]; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; @@ -196,6 +199,13 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.formIndex = config.formIndex; } + // Set scale + if (!isNullOrUndefined(config.spriteScale)) { + enemyPokemon.mysteryEncounterData = { + spriteScale: config.spriteScale + }; + } + // Set Boss if (config.isBoss) { let segments = !isNullOrUndefined(config.bossSegments) ? config.bossSegments : scene.getEncounterBossSegments(scene.currentBattle.waveIndex, level, enemySpecies, true); @@ -206,12 +216,22 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: } // Set Passive - if (partyConfig.pokemonConfigs[e].passive) { + if (config.passive) { enemyPokemon.passive = true; } + // Set Nature + if (config.nature) { + enemyPokemon.nature = config.nature; + } + + // Set IVs + if (config.ivs) { + enemyPokemon.ivs = config.ivs; + } + // Set Status - const statusEffects = partyConfig.pokemonConfigs[e].status; + const statusEffects = config.status; if (statusEffects) { // Default to cureturn 3 for sleep const status = Array.isArray(statusEffects) ? statusEffects[0] : statusEffects; @@ -811,65 +831,3 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n console.log(`Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Uncommons: ${uncommonMean}\nAvg Rares: ${rareMean}\nAvg Super Rares: ${superRareMean}`); } - -/** - * Takes care of handling player pokemon KO (with all its side effects) - * - * @param scene the battle scene - * @param pokemon the player pokemon to KO - */ -export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) { - pokemon.hp = 0; - pokemon.trySetStatus(StatusEffect.FAINT); - pokemon.updateInfo(); - queueEncounterMessage(scene, i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); -} - -/** - * Handles applying hp changes to a player pokemon. - * Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info. - * TODO: handle special cases like wonder-guard/ninjask - * @param scene the battle scene - * @param pokemon the player pokemon to apply the hp change to - * @param value the hp change amount. Positive for heal. Negative for damage - * - */ -function applyHpChangeToPokemon(scene: BattleScene, pokemon: PlayerPokemon, value: number) { - const hpChange = Math.round(pokemon.hp + value); - const nextHp = Math.max(Math.min(hpChange, pokemon.getMaxHp()), 0); - if (nextHp === 0) { - koPlayerPokemon(scene, pokemon); - } else { - pokemon.hp = nextHp; - } -} - -/** - * Handles applying damage to a player pokemon - * @param scene the battle scene - * @param pokemon the player pokemon to apply damage to - * @param damage the amount of damage to apply - * @see {@linkcode applyHpChangeToPokemon} - */ -export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, damage: number) { - if (damage <= 0) { - console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead."); - } - - applyHpChangeToPokemon(scene, pokemon, -damage); -} - -/** - * Handles applying heal to a player pokemon - * @param scene the battle scene - * @param pokemon the player pokemon to apply heal to - * @param heal the amount of heal to apply - * @see {@linkcode applyHpChangeToPokemon} - */ -export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) { - if (heal <= 0) { - console.warn("Damaging pokemong with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead."); - } - - applyHpChangeToPokemon(scene, pokemon, heal); -} diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 76c6cb9c680..2dbbe9f55a1 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -15,7 +15,12 @@ import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { Species } from "#enums/species"; import { Type } from "#app/data/type"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; -import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; + +export interface MysteryEncounterPokemonData { + spriteScale?: number +} /** * @@ -131,10 +136,81 @@ export function getRandomSpeciesByStarterTier(starterTiers: number | [number, nu return Species.BULBASAUR; } -export function koPlayerPokemon(pokemon: PlayerPokemon) { +/** + * Takes care of handling player pokemon KO (with all its side effects) + * + * @param scene the battle scene + * @param pokemon the player pokemon to KO + */ +export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) { pokemon.hp = 0; pokemon.trySetStatus(StatusEffect.FAINT); pokemon.updateInfo(); + queueEncounterMessage(scene, i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); +} + +/** + * Handles applying hp changes to a player pokemon. + * Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info. + * TODO: handle special cases like wonder-guard/ninjask + * @param scene the battle scene + * @param pokemon the player pokemon to apply the hp change to + * @param value the hp change amount. Positive for heal. Negative for damage + * + */ +function applyHpChangeToPokemon(scene: BattleScene, pokemon: PlayerPokemon, value: number) { + const hpChange = Math.round(pokemon.hp + value); + const nextHp = Math.max(Math.min(hpChange, pokemon.getMaxHp()), 0); + if (nextHp === 0) { + koPlayerPokemon(scene, pokemon); + } else { + pokemon.hp = nextHp; + } +} + +/** + * Handles applying damage to a player pokemon + * @param scene the battle scene + * @param pokemon the player pokemon to apply damage to + * @param damage the amount of damage to apply + * @see {@linkcode applyHpChangeToPokemon} + */ +export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, damage: number) { + if (damage <= 0) { + console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead."); + } + + applyHpChangeToPokemon(scene, pokemon, -damage); +} + +/** + * Handles applying heal to a player pokemon + * @param scene the battle scene + * @param pokemon the player pokemon to apply heal to + * @param heal the amount of heal to apply + * @see {@linkcode applyHpChangeToPokemon} + */ +export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) { + if (heal <= 0) { + console.warn("Damaging pokemong with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead."); + } + + applyHpChangeToPokemon(scene, pokemon, heal); +} + +/** + * Will modify all of a Pokemon's base stats by a flat value + * Base stats can never go below 1 + * @param pokemon + * @param value + */ +export function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { + pokemon.getSpeciesForm().baseStats = [...pokemon.getSpeciesForm().baseStats].map(v => { + const newVal = Math.floor(v + value); + return Math.max(newVal, 1); + }); + pokemon.calculateStats(); + pokemon.updateInfo(); } /** diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 2b488f330c4..26af9155687 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -221,6 +221,14 @@ export abstract class PokemonSpeciesForm { return false; } + /** + * Gets the BST for the species + * @returns The species' BST. + */ + getBaseStatTotal(): integer { + return this.baseStats.reduce((i, n) => n + i); + } + /** * Gets the species' base stat amount for the given stat. * @param stat The desired stat. diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 348b7c9f398..d4d0b479f9c 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -10,5 +10,6 @@ export enum MysteryEncounterType { FIELD_TRIP, SAFARI_ZONE, LOST_AT_SEA, //might be generalized later on - FIERY_FALLOUT + FIERY_FALLOUT, + THE_STRONG_STUFF } diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 9ae83f49fab..84f6488c439 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -41,7 +41,7 @@ export class MysteryEncounterSpriteConfig { /** Y offset */ y?: number; /** Y shadow offset */ - yShadowOffset?: number; + yShadow?: number; /** Sprite scale. `0` - `n` */ scale?: number; /** If you are using an item sprite, set to `true` */ @@ -72,10 +72,10 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return; } - const getSprite = (spriteKey: string, hasShadow?: boolean, yShadowOffset?: number) => { + const getSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => { const ret = this.scene.addFieldSprite(0, 0, spriteKey); ret.setOrigin(0.5, 1); - ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadowOffset ?? 0 }); + ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 }); return ret; }; @@ -94,13 +94,13 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1)); this.spriteConfigs?.forEach((config) => { - const { spriteKey, isItem, hasShadow, scale, x, y, yShadowOffset, alpha } = config; + const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config; let sprite: GameObjects.Sprite; let tintSprite: GameObjects.Sprite; if (!isItem) { - sprite = getSprite(spriteKey, hasShadow, yShadowOffset); + sprite = getSprite(spriteKey, hasShadow, yShadow); tintSprite = getSprite(spriteKey); } else { sprite = getItemSprite(spriteKey); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b0974bd0aa8..8b2e65e4abb 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -50,6 +50,7 @@ import { BerryType } from "#enums/berry-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; export enum FieldPosition { CENTER, @@ -100,6 +101,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleData: PokemonBattleData; public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; + public mysteryEncounterData: MysteryEncounterPokemonData; /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects: (pokemon: Pokemon) => void = null; @@ -524,6 +526,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const formKey = this.getFormKey(); if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) { return 1.5; + } else if (this?.mysteryEncounterData?.spriteScale) { + return this.mysteryEncounterData.spriteScale; } return 1; } diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 817daa19bf8..891d6dc299b 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -10,6 +10,7 @@ import { safariZoneDialogue } from "#app/locales/en/mystery-encounters/safari-zo import { shadyVitaminDealerDialogue } from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue"; import { slumberingSnorlaxDialogue } from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue"; import { trainingSessionDialogue } from "#app/locales/en/mystery-encounters/training-session-dialogue"; +import { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue"; /** * Patterns that can be used: @@ -44,4 +45,5 @@ export const mysteryEncounter = { safariZone: safariZoneDialogue, lostAtSea: lostAtSeaDialogue, fieryFallout: fieryFalloutDialogue, + theStrongStuff: theStrongStuffDialogue, } as const; diff --git a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts new file mode 100644 index 00000000000..c65019b539f --- /dev/null +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts @@ -0,0 +1,25 @@ +export const theStrongStuffDialogue = { + intro: "It's a massive Shuckle and what appears\nto be an equally large stash of... juice?", + title: "The Strong Stuff", + description: "The Shuckle that blocks your path looks incredibly strong. Meanwhile, the juice next to it is emanating power of some kind.\n\nThe Shuckle extends its feelers in your direction. It seems like it wants to touch you, but is that really a good idea?", + query: "What will you do?", + option: { + 1: { + label: "Let it touch you", + tooltip: "(?) Something awful or amazing might happen", + selected: "You black out.", + selected_2: `@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained. + $Your {{highBstPokemon}} feels a\nterrible lethargy come over it! + $It's base stats were reduced by 20 in each stat! + $Your remaining Pokémon feel an incredible vigor, though! + $Their base stats are increased by 10 in each stat!` + }, + 2: { + label: "Battle the Shuckle", + tooltip: "(-) Hard Battle\n(+) Special Rewards", + selected: "Enraged, the Shuckle drinks some of its juice and attacks!", + stat_boost: "The Shuckle's juice boosts its stats!", + }, + }, + outro: "What a bizarre turn of events." +}; diff --git a/src/phases.ts b/src/phases.ts index 87e8cde9f5d..0ceff19f059 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1107,18 +1107,19 @@ export class EncounterPhase extends BattlePhase { if (showEncounterMessage) { const introDialogue = this.scene.currentBattle.mysteryEncounter.dialogue.intro; + const FIRST_DIALOGUE_PROMPT_DELAY = 750; let i = 0; const showNextDialogue = () => { const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue; const dialogue = introDialogue[i]; const title = getEncounterText(this.scene, dialogue.speaker); const text = getEncounterText(this.scene, dialogue.text); - if (title) { - this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0); - } else { - this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true); - } i++; + if (title) { + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text, null, nextAction, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } }; if (introDialogue.length > 0) { @@ -1686,6 +1687,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); pokemon.getSprite().clearTint(); pokemon.resetSummonData(); + this.scene.updateFieldScale(); this.scene.time.delayedCall(1000, () => this.end()); } }); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 11ff4766695..d55bcaad636 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -26,10 +26,9 @@ import { BattlerTagLapseType } from "#app/data/battler-tags"; * - Queuing of the MysteryEncounterOptionSelectedPhase */ export class MysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 300; optionSelectSettings: OptionSelectSettings; - private FIRST_DIALOGUE_PROMPT_DELAY = 300; - /** * * @param scene @@ -108,12 +107,12 @@ export class MysteryEncounterPhase extends Phase { title = getEncounterText(this.scene, dialogue.speaker); } - if (title) { - this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); - } else { - this.scene.ui.showText(text, null, nextAction, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); - } i++; + if (title) { + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text, null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } }; showNextDialogue(); @@ -420,6 +419,7 @@ export class MysteryEncounterRewardsPhase extends Phase { * - Queuing of the next wave */ export class PostMysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 750; onPostOptionSelect: OptionPhaseCallback; constructor(scene: BattleScene) { @@ -462,13 +462,13 @@ export class PostMysteryEncounterPhase extends Phase { title = getEncounterText(this.scene, dialogue.speaker); } + i++; this.scene.ui.setMode(Mode.MESSAGE); if (title) { - this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0); + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); } else { - this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true); + this.scene.ui.showText(text, null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); } - i++; }; showNextDialogue(); diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts new file mode 100644 index 00000000000..cbc5ab7950e --- /dev/null +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -0,0 +1,239 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils"; +import { SelectModifierPhase } from "#app/phases"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { DepartmentStoreSaleEncounter } from "#app/data/mystery-encounters/encounters/department-store-sale-encounter"; +import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; + +const namespace = "mysteryEncounter:departmentStoreSale"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 37; + +describe("Department Store Sale - 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.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(true); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.DEPARTMENT_STORE_SALE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + + expect(DepartmentStoreSaleEncounter.encounterType).toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + expect(DepartmentStoreSaleEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(DepartmentStoreSaleEncounter.dialogue).toBeDefined(); + expect(DepartmentStoreSaleEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}:intro` }, + { + speaker: `${namespace}:speaker`, + text: `${namespace}:intro_dialogue`, + } + ]); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`); + expect(DepartmentStoreSaleEncounter.options.length).toBe(4); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + describe("Option 1 - TM Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[0]; + expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + }); + }); + + it("should have shop with only TMs", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 1); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("TM_"); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Vitamin Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[1]; + expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + }); + }); + + it("should have shop with only Vitamins", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 2); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id.includes("PP_UP") || + option.modifierTypeOption.type.id.includes("BASE_STAT_BOOSTER")).toBeTruthy(); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - X Item Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[2]; + expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + }); + }); + + it("should have shop with only X Items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 3); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id.includes("DIRE_HIT") || + option.modifierTypeOption.type.id.includes("TEMP_STAT_BOOSTER")).toBeTruthy(); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 4 - Pokeball Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[3]; + expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:4:label`, + buttonTooltip: `${namespace}:option:4:tooltip`, + }); + }); + + it("should have shop with only Pokeballs", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 4); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BALL"); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runSelectMysteryEncounterOption(game, 4); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 20d0426e02d..33845d1dfe5 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -24,7 +24,7 @@ const namespace = "mysteryEncounter:fieryFallout"; /** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ const defaultParty = [Species.ARCANINE, Species.NINETALES, Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultBiome = Biome.VOLCANO; -const defaultWave = 45; +const defaultWave = 56; describe("Fiery Fallout - Mystery Encounter", () => { let phaserGame: Phaser.Game; @@ -42,7 +42,6 @@ describe("Fiery Fallout - Mystery Encounter", () => { game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.startingWave(defaultWave); game.override.startingBiome(defaultBiome); - game.override.disableTrainerWave(true); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -54,10 +53,11 @@ describe("Fiery Fallout - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); }); it("should have the correct properties", async () => { - game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); expect(FieryFalloutEncounter.encounterType).toBe(MysteryEncounterType.FIERY_FALLOUT); @@ -74,7 +74,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { game.override.startingBiome(Biome.MOUNTAIN); await game.runToMysteryEncounter(); - expect(scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); }); it("should not run below wave 41", async () => { @@ -82,7 +82,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { await game.runToMysteryEncounter(); - expect(scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); }); it("should not run above wave 179", async () => { @@ -96,7 +96,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should initialize fully ", async () => { vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: FieryFalloutEncounter } as Battle); const weatherSpy = vi.spyOn(scene.arena, "trySetWeather").mockReturnValue(true); - const moveInitSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); const { onInit } = FieryFalloutEncounter; @@ -130,10 +130,6 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); describe("Option 1 - Fight 2 Volcarona", () => { - beforeEach(async () => { - game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); - }); - it("should have the correct properties", () => { const option1 = FieryFalloutEncounter.options[0]; expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); @@ -180,14 +176,10 @@ describe("Fiery Fallout - Mystery Encounter", () => { && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); expect(charcoal).toBeDefined; - }, 100000000); + }); }); describe("Option 2 - Suffer the weather", () => { - beforeEach(async () => { - game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); - }); - it("should have the correct properties", () => { const option1 = FieryFalloutEncounter.options[1]; expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); @@ -235,10 +227,6 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); describe("Option 3 - use FIRE types", () => { - beforeEach(async () => { - game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); - }); - it("should have the correct properties", () => { const option1 = FieryFalloutEncounter.options[2]; expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_SPECIAL); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 04854574a77..6d36f47a8c1 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -32,7 +32,6 @@ describe("Lost at Sea - Mystery Encounter", () => { game.override.mysteryEncounterChance(100); game.override.startingWave(defaultWave); game.override.startingBiome(defaultBiome); - game.override.disableTrainerWave(true); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -44,10 +43,11 @@ describe("Lost at Sea - Mystery Encounter", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); }); it("should have the correct properties", async () => { - game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); expect(LostAtSeaEncounter.encounterType).toBe(MysteryEncounterType.LOST_AT_SEA); @@ -99,10 +99,6 @@ describe("Lost at Sea - Mystery Encounter", () => { }); describe("Option 1 - Surf", () => { - beforeEach(async () => { - game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); - }); - it("should have the correct properties", () => { const option1 = LostAtSeaEncounter.options[0]; expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_DEFAULT); @@ -149,10 +145,6 @@ describe("Lost at Sea - Mystery Encounter", () => { }); describe("Option 2 - Fly", () => { - beforeEach(async () => { - game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); - }); - it("should have the correct properties", () => { const option2 = LostAtSeaEncounter.options[1]; @@ -202,10 +194,6 @@ describe("Lost at Sea - Mystery Encounter", () => { }); describe("Option 3 - Wander aimlessy", () => { - beforeEach(async () => { - game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); - }); - it("should have the correct properties", () => { const option3 = LostAtSeaEncounter.options[2]; diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts new file mode 100644 index 00000000000..ed15380b8c9 --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -0,0 +1,230 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Battle from "#app/battle"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import * as Modifiers from "#app/modifier/modifier"; +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; +import { Nature } from "#app/data/nature"; +import { BerryType } from "#enums/berry-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; + +const namespace = "mysteryEncounter:theStrongStuff"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Strong Stuff - 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.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.THE_STRONG_STUFF]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + + expect(TheStrongStuffEncounter.encounterType).toBe(MysteryEncounterType.THE_STRONG_STUFF); + expect(TheStrongStuffEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(TheStrongStuffEncounter.dialogue).toBeDefined(); + expect(TheStrongStuffEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}:intro` }]); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`); + expect(TheStrongStuffEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of CAVE biome", async () => { + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully ", async () => { + vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: TheStrongStuffEncounter } as Battle); + const moveInitSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = TheStrongStuffEncounter; + + expect(TheStrongStuffEncounter.onInit).toBeDefined(); + + const onInitResult = onInit(scene); + + expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([ + { + levelAdditiveMultiplier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SHUCKLE), + isBoss: true, + bossSegments: 5, + spriteScale: 1.5, + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierTypes: expect.any(Array), + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: expect.any(Function) + } + ], + } + ]); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Power Swap BSTs", () => { + it("should have the correct properties", () => { + const option1 = TheStrongStuffEncounter.options[0]; + expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + selected: [ + { + text: `${namespace}:option:1:selected`, + }, + ], + }); + }); + + it("should lower stats of highest BST and raise stats for rest of party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + + const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal()); + await runSelectMysteryEncounterOption(game, 1); + + const bstsAfter = scene.getParty().map(p => { + return p.getSpeciesForm().getBaseStatTotal(); + }); + + expect(bstsAfter[0]).toEqual(bstsPrior[0] - 20 * 6); + expect(bstsAfter[1]).toEqual(bstsPrior[1] + 10 * 6); + expect(bstsAfter[2]).toEqual(bstsPrior[2] + 10 * 6); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runSelectMysteryEncounterOption(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - battle the Shuckle", () => { + it("should have the correct properties", () => { + const option1 = TheStrongStuffEncounter.options[1]; + expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], + }); + }); + + it("should start battle against Shuckle", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runSelectMysteryEncounterOption(game, 2, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE); + expect(enemyField[0].summonData.battleStats).toEqual([0, 2, 0, 2, 0, 0, 0]); + const shuckleItems = scene.getModifiers(Modifiers.BerryModifier, false); + expect(shuckleItems.length).toBe(4); + expect(shuckleItems.find(m => m.berryType === BerryType.SITRUS)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m.berryType === BerryType.GANLON)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m.berryType === BerryType.APICOT)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m.berryType === BerryType.LUM)?.stackCount).toBe(2); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.INFESTATION), new PokemonMove(Moves.SALT_CURE), new PokemonMove(Moves.GASTRO_ACID), new PokemonMove(Moves.HEAL_ORDER)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(2); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.GASTRO_ACID).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STEALTH_ROCK).length).toBe(1); + }); + + it("should have Soul Dew in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runSelectMysteryEncounterOption(game, 2, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.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).toEqual("SOUL_DEW"); + }); + }); +}); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index 2867bcc63bc..cdc7eda180f 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -243,7 +243,7 @@ describe("Mystery Encounter Utils", () => { arceus.hp = 100; expect(arceus.isAllowedInBattle()).toBe(true); - koPlayerPokemon(arceus); + koPlayerPokemon(scene, arceus); expect(arceus.isAllowedInBattle()).toBe(false); }); }); diff --git a/src/test/mystery-encounter/mystery-encounter.test.ts b/src/test/mystery-encounter/mystery-encounter.test.ts index ef0b5b3238a..0ba28bb77ec 100644 --- a/src/test/mystery-encounter/mystery-encounter.test.ts +++ b/src/test/mystery-encounter/mystery-encounter.test.ts @@ -23,8 +23,6 @@ describe("Mystery Encounters", () => { game = new GameManager(phaserGame); game.override.startingWave(11); game.override.mysteryEncounterChance(100); - game.override.mysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); - game.override.disableTrainerWave(true); }); it("Spawns a mystery encounter", async () => { diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts index 2a7d3de3700..5c7c9958c80 100644 --- a/src/test/phases/mystery-encounter-phase.test.ts +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -28,7 +28,6 @@ describe("Mystery Encounter Phases", () => { game = new GameManager(phaserGame); game.override.startingWave(11); game.override.mysteryEncounterChance(100); - game.override.mysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); // Seed guarantees wild encounter to be replaced by ME game.override.seed("test"); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 7ccec8113b3..a8bd8ee3404 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -38,6 +38,7 @@ import {MysteryEncounterPhase} from "#app/phases/mystery-encounter-phases"; import { OverridesHelper } from "./overridesHelper"; import { expect } from "vitest"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { isNullOrUndefined } from "#app/utils"; /** * Class to manage the game state and transitions between phases. @@ -151,6 +152,11 @@ export default class GameManager { * @returns A promise that resolves when the EncounterPhase ends. */ async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) { + if (!isNullOrUndefined(encounterType)) { + this.override.disableTrainerWaves(true); + this.override.mysteryEncounter(encounterType); + } + await this.runToTitle(); this.onNextPrompt("TitlePhase", Mode.TITLE, () => { @@ -167,7 +173,7 @@ export default class GameManager { }, () => this.isCurrentPhase(MysteryEncounterPhase), true); await this.phaseInterceptor.run(EncounterPhase); - if (encounterType) { + if (!isNullOrUndefined(encounterType)) { expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); } } diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts index 4116810be5d..d2011bf4581 100644 --- a/src/test/utils/overridesHelper.ts +++ b/src/test/utils/overridesHelper.ts @@ -6,6 +6,8 @@ import GameManager from "#test/utils/gameManager"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import * as overrides from "#app/overrides"; import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import * as GameMode from "#app/game-mode"; +import { GameModes, getGameMode } from "#app/game-mode"; /** * Helper to handle overrides in tests @@ -77,8 +79,13 @@ export class OverridesHelper { * @returns spy instance * @param disable - true */ - disableTrainerWave(disable: boolean): MockInstance { - const spy = vi.spyOn(this.game.scene.gameMode, "isWaveTrainer").mockReturnValue(!disable); + disableTrainerWaves(disable: boolean): MockInstance { + const realFn = getGameMode; + const spy = vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => { + const mode = realFn(gameMode); + mode.hasTrainers = !disable; + return mode; + }); this.log(`Standard trainer waves are ${disable? "disabled" : "enabled"}!`); return spy; } diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index 05c91ca1643..cdd8b20b09d 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -32,7 +32,8 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { const charVarMap = new Map(); const delayMap = new Map(); const soundMap = new Map(); - const actionPattern = /@(c|d|s)\{(.*?)\}/; + const fadeMap = new Map(); + const actionPattern = /@(c|d|s|f)\{(.*?)\}/; let actionMatch: RegExpExecArray; while ((actionMatch = actionPattern.exec(text))) { switch (actionMatch[1]) { @@ -45,6 +46,9 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { case "s": soundMap.set(actionMatch.index, actionMatch[2]); break; + case "f": + fadeMap.set(actionMatch.index, parseInt(actionMatch[2])); + break; } text = text.slice(0, actionMatch.index) + text.slice(actionMatch.index + actionMatch[2].length + 4); } @@ -103,6 +107,7 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { const charVar = charVarMap.get(charIndex); const charSound = soundMap.get(charIndex); const charDelay = delayMap.get(charIndex); + const charFade = fadeMap.get(charIndex); this.message.setText(text.slice(0, charIndex)); const advance = () => { if (charVar) { @@ -134,6 +139,19 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { advance(); } }); + } else if (charFade) { + this.textTimer.paused = true; + this.scene.time.delayedCall(150, () => { + this.scene.ui.fadeOut(750).then(() => { + const delay = Utils.getFrameMs(charFade); + this.scene.time.delayedCall(delay, () => { + this.scene.ui.fadeIn(500).then(() => { + this.textTimer.paused = false; + advance(); + }); + }); + }); + }); } else { advance(); } diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index cfb2dfbd3a8..72cce41d55f 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -12,6 +12,7 @@ import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -135,7 +136,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { // TODO: If we need to handle cancel option? Maybe default logic to leave/run from encounter idk } } else { - switch (this.optionsContainer.list.length) { + switch (this.optionsContainer.getAll()?.length) { + default: case 3: success = this.handleTwoOptionMoveInput(button); break; @@ -284,7 +286,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.cursor = cursor; } - this.viewPartyIndex = this.optionsContainer.length - 1; + this.viewPartyIndex = this.optionsContainer.getAll()?.length - 1; if (!this.cursorObj) { this.cursorObj = this.scene.add.image(0, 0, "cursor"); @@ -293,11 +295,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (cursor === this.viewPartyIndex) { this.cursorObj.setPosition(246, -17); - } else if (this.optionsContainer.length === 3) { // 2 Options + } else if (this.optionsContainer.getAll()?.length === 3) { // 2 Options this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15); - } else if (this.optionsContainer.length === 4) { // 3 Options + } else if (this.optionsContainer.getAll()?.length === 4) { // 3 Options this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0)); - } else if (this.optionsContainer.length === 5) { // 4 Options + } else if (this.optionsContainer.getAll()?.length === 5) { // 4 Options this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0)); } @@ -368,7 +370,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5); // Rarity of encounter - const ballType = getPokeballAtlasKey(mysteryEncounter.encounterTier as number); + const index = mysteryEncounter.encounterTier === MysteryEncounterTier.COMMON ? 0 : + mysteryEncounter.encounterTier === MysteryEncounterTier.GREAT ? 1 : + mysteryEncounter.encounterTier === MysteryEncounterTier.ULTRA ? 2 : + mysteryEncounter.encounterTier === MysteryEncounterTier.ROGUE ? 3 : 4; + const ballType = getPokeballAtlasKey(index); this.rarityBall.setTexture("pb", ballType); const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });