add unit tests for clowning around

This commit is contained in:
ImperialSympathizer 2024-08-07 22:52:12 -04:00
parent afe1015094
commit 42eadcb36c
12 changed files with 480 additions and 159 deletions

View File

@ -1,41 +0,0 @@
{
"textures": [
{
"image": "encounter_radar.png",
"format": "RGBA8888",
"size": {
"w": 17,
"h": 16
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 15,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 15,
"h": 14
},
"frame": {
"x": 1,
"y": 1,
"w": 15,
"h": 14
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:eb3445f19546ab36edb2909c89b8aa86:c8de156a28ef70ee4ddf70cffe1ba3ba:e7008b81ccf0cb0325145a809afa6165$"
}
}

View File

@ -1,41 +0,0 @@
{
"textures": [
{
"image": "exclaim.png",
"format": "RGBA8888",
"size": {
"w": 32,
"h": 32
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 32
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 32
},
"frame": {
"x": 0,
"y": 0,
"w": 32,
"h": 32
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
}
}

View File

@ -534,13 +534,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> {
const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim];
const encounterAnimNames = Utils.getEnumKeys(EncounterAnim);
// const encounterAnimIds = Utils.getEnumValues(EncounterAnim);
const encounterAnimFetches = [];
for (const anim of anims) {
if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) {
continue;
}
// const encounterAnimId = encounterAnimIds[anim];
encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`)
.then(response => response.json())
.then(cas => encounterAnims.set(anim, new AnimConfig(cas))));

View File

@ -1,7 +1,7 @@
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength";
import BattleScene from "#app/battle-scene";
@ -104,16 +104,14 @@ export const ClowningAroundEncounter: IMysteryEncounter =
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
// Clown trainer is pulled from pool of boss trainers (gym leaders) for the biome
// They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons
const clownTrainerType = TrainerType.HARLEQUIN;
const clownConfig = trainerConfigs[clownTrainerType].copy();
const clownPartyTemplate = new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(1, PartyMemberStrength.STRONG),
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER));
const clownConfig = trainerConfigs[clownTrainerType].copy();
clownConfig.setPartyTemplates(clownPartyTemplate);
clownConfig.setDoubleOnly();
clownConfig.partyTemplateFunc = null; // Overrides party template func
clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists
// Generate random ability for Blacephalon from pool
const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)];
@ -142,28 +140,6 @@ export const ClowningAroundEncounter: IMysteryEncounter =
// Load animations/sfx for start of fight moves
loadCustomMovesForEncounter(scene, [Moves.ROLE_PLAY, Moves.TAUNT]);
// These have to be defined at runtime so that modifierTypes exist
encounter.misc.RANDOM_ULTRA_POOL = [
modifierTypes.REVIVER_SEED,
modifierTypes.GOLDEN_PUNCH,
modifierTypes.ATTACK_TYPE_BOOSTER,
modifierTypes.QUICK_CLAW,
modifierTypes.WIDE_LENS,
modifierTypes.WHITE_HERB
];
encounter.misc.RANDOM_ROGUE_POOL = [
modifierTypes.LEFTOVERS,
modifierTypes.SHELL_BELL,
modifierTypes.SOUL_DEW,
modifierTypes.SOOTHE_BELL,
modifierTypes.SCOPE_LENS,
modifierTypes.BATON,
modifierTypes.FOCUS_BAND,
modifierTypes.KINGS_ROCK,
modifierTypes.GRIP_CLAW
];
return true;
})
.withTitle(`${namespace}.title`)
@ -187,7 +163,7 @@ export const ClowningAroundEncounter: IMysteryEncounter =
// Spawn battle
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true });
setEncounterRewards(scene, { fillRemaining: true });
// TODO: when Magic Room and Wonder Room are implemented, add those to start of battle
encounter.startOfBattleEffects.push(
@ -217,7 +193,6 @@ export const ClowningAroundEncounter: IMysteryEncounter =
// After the battle, offer the player the opportunity to permanently swap ability
const abilityWasSwapped = await handleSwapAbility(scene);
if (abilityWasSwapped) {
await scene.ui.setMode(Mode.MESSAGE);
await showEncounterText(scene, `${namespace}.option.1.ability_gained`);
}
@ -284,44 +259,33 @@ export const ClowningAroundEncounter: IMysteryEncounter =
const items = mostHeldItemsPokemon.getHeldItems();
// Shuffles Berries (if they have any)
const berries = items.filter(m => m instanceof BerryModifier);
let numBerries = 0;
items.filter(m => m instanceof BerryModifier)
.forEach(m => {
numBerries += m.stackCount;
scene.removeModifier(m);
});
berries.forEach(berry => {
const stackCount = berry.stackCount;
scene.removeModifier(berry);
const newBerry = generateModifierTypeOption(scene, modifierTypes.BERRY, [randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType]).type as BerryModifierType;
for (let i = 0; i < stackCount; i++) {
applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newBerry);
}
});
generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries");
// Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm)
const transferableItems = items.filter(m => m.isTransferrable && !(m instanceof BerryModifier));
transferableItems.forEach(transferableItem => {
const stackCount = transferableItem.stackCount;
transferableItem.type.withTierFromPool();
// Lucky Eggs and other items that do not appear in item pools are treated as Ultra rarity
const tier = transferableItem.type.tier ?? ModifierTier.ULTRA;
if (tier === ModifierTier.ULTRA) {
scene.removeModifier(transferableItem);
for (let i = 0; i < stackCount; i++) {
const newItemType = encounter.misc.RANDOM_ULTRA_POOL[randSeedInt(encounter.misc.RANDOM_ULTRA_POOL.length)];
const newMod = generateModifierTypeOption(scene, newItemType).type as PokemonHeldItemModifierType;
applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newMod);
let numUltra = 0;
let numRogue = 0;
items.filter(m => m.isTransferrable && !(m instanceof BerryModifier))
.forEach(m => {
const type = m.type.withTierFromPool();
const tier = type.tier ?? ModifierTier.ULTRA;
if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) {
numUltra += m.stackCount;
scene.removeModifier(m);
} else if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
numRogue += m.stackCount;
scene.removeModifier(m);
}
} else if (tier === ModifierTier.ROGUE) {
scene.removeModifier(transferableItem);
for (let i = 0; i < stackCount; i++) {
const newItemType = encounter.misc.RANDOM_ROGUE_POOL[randSeedInt(encounter.misc.RANDOM_ROGUE_POOL.length)];
const newMod = generateModifierTypeOption(scene, newItemType).type as PokemonHeldItemModifierType;
applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newMod);
}
}
});
});
generateItemsOfTier(scene, mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA);
generateItemsOfTier(scene, mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE);
})
.withOptionPhase(async (scene: BattleScene) => {
leaveEncounterWithoutBattle(scene, true);
@ -456,7 +420,7 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
}
pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter.misc.ability;
scene.currentBattle.mysteryEncounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
resolve(true);
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));
};
const onPokemonNotSelected = () => {
@ -467,3 +431,67 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected);
}
function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: integer, tier: ModifierTier | "Berries") {
// These pools have to be defined at runtime so that modifierTypes exist
// Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon
// This is to prevent "over-generating" a random item of a certain type during item swaps
const ultraPool = [
[modifierTypes.REVIVER_SEED, 1],
[modifierTypes.GOLDEN_PUNCH, 5],
[modifierTypes.ATTACK_TYPE_BOOSTER, 99],
[modifierTypes.QUICK_CLAW, 3],
[modifierTypes.WIDE_LENS, 3],
[modifierTypes.WHITE_HERB, 2]
];
const roguePool = [
[modifierTypes.LEFTOVERS, 4],
[modifierTypes.SHELL_BELL, 4],
[modifierTypes.SOUL_DEW, 10],
[modifierTypes.SOOTHE_BELL, 3],
[modifierTypes.SCOPE_LENS, 5],
[modifierTypes.BATON, 1],
[modifierTypes.FOCUS_BAND, 5],
[modifierTypes.KINGS_ROCK, 3],
[modifierTypes.GRIP_CLAW, 5]
];
const berryPool = [
[BerryType.APICOT, 3],
[BerryType.ENIGMA, 2],
[BerryType.GANLON, 3],
[BerryType.LANSAT, 3],
[BerryType.LEPPA, 2],
[BerryType.LIECHI, 3],
[BerryType.LUM, 2],
[BerryType.PETAYA, 3],
[BerryType.SALAC, 2],
[BerryType.SITRUS, 2],
[BerryType.STARF, 3]
];
let pool: any[];
if (tier === "Berries") {
pool = berryPool;
} else {
pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool;
}
for (let i = 0; i < numItems; i++) {
const randIndex = randSeedInt(pool.length);
const newItemType = pool[randIndex];
let newMod;
if (tier === "Berries") {
newMod = generateModifierTypeOption(scene, modifierTypes.BERRY, [newItemType[0]]).type as PokemonHeldItemModifierType;
} else {
newMod = generateModifierTypeOption(scene, newItemType[0]).type as PokemonHeldItemModifierType;
}
applyModifierTypeToPlayerPokemon(scene, pokemon, newMod);
// Decrement max stacks and remove from pool if at max
newItemType[1]--;
if (newItemType[1] <= 0) {
pool.splice(randIndex, 1);
}
}
}

View File

@ -38,7 +38,7 @@ import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/myster
* @param scene
*/
export function doTrainerExclamation(scene: BattleScene) {
const exclamationSprite = scene.addFieldSprite(0, 0, "exclaim");
const exclamationSprite = scene.add.sprite(0, 0, "exclaim");
exclamationSprite.setName("exclamation");
scene.field.add(exclamationSprite);
scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1);
@ -386,10 +386,12 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M
*/
export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: (pokemon: PlayerPokemon) => string): Promise<boolean> {
return new Promise(resolve => {
const modeToSetOnExit = scene.ui.getMode();
// Open party screen to choose pokemon to train
scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => {
if (slotIndex < scene.getParty().length) {
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => {
scene.ui.setMode(modeToSetOnExit).then(() => {
const pokemon = scene.getParty()[slotIndex];
const secondaryOptions = onPokemonSelected(pokemon);
if (!secondaryOptions) {
@ -443,7 +445,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
});
});
} else {
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => {
scene.ui.setMode(modeToSetOnExit).then(() => {
if (onPokemonNotSelected) {
onPokemonNotSelected();
}

View File

@ -190,9 +190,6 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
}
});
// Load dex progress icon
this.scene.loadAtlas("encounter_radar", "mystery-encounters");
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
this.spriteConfigs.every((config) => {
if (config.isItem) {

View File

@ -275,6 +275,9 @@ export class LoadingScene extends SceneBase {
}
}
// Load Mystery Encounter dex progress icon
this.loadImage("encounter_radar", "mystery-encounters");
this.loadAtlas("dualshock", "inputs");
this.loadAtlas("xbox", "inputs");
this.loadAtlas("keyboard", "inputs");

View File

@ -127,9 +127,9 @@ class DefaultOverrides {
// -------------------------
// 1 to 256, set to null to ignore
readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null;
readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256;
readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null;
readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null;
readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.TRAINING_SESSION;
// -------------------------
// MODIFIER / ITEM OVERRIDES

View File

@ -915,7 +915,7 @@ export class EncounterPhase extends BattlePhase {
// Load Mystery Encounter Exclamation bubble and sfx
loadEnemyAssets.push(new Promise<void>(resolve => {
this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav");
this.scene.loadAtlas("exclaim", "mystery-encounters");
this.scene.loadImage("exclaim", "mystery-encounters");
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve());
if (!this.scene.load.isLoading()) {
this.scene.load.start();

View File

@ -0,0 +1,374 @@
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 { 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 { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { CommandPhase, MovePhase, NewBattlePhase, SelectModifierPhase } from "#app/phases";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { Mode } from "#app/ui/ui";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter";
import { TrainerType } from "#enums/trainer-type";
import { Abilities } from "#enums/abilities";
import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { Button } from "#enums/buttons";
import PartyUiHandler from "#app/ui/party-ui-handler";
import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { BerryType } from "#enums/berry-type";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { Type } from "#app/data/type";
const namespace = "mysteryEncounter:clowningAround";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("Clowning Around - 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, MysteryEncounterType[]>([
[Biome.CAVE, [MysteryEncounterType.CLOWNING_AROUND]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
expect(ClowningAroundEncounter.encounterType).toBe(MysteryEncounterType.CLOWNING_AROUND);
expect(ClowningAroundEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA);
expect(ClowningAroundEncounter.dialogue).toBeDefined();
expect(ClowningAroundEncounter.dialogue.intro).toStrictEqual([
{ text: `${namespace}.intro` },
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`,
},
]);
expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`);
expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`);
expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`);
expect(ClowningAroundEncounter.options.length).toBe(3);
});
it("should not run below wave 80", async () => {
game.override.startingWave(79);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND);
});
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 () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ClowningAroundEncounter;
const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim");
const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets");
const { onInit } = ClowningAroundEncounter;
expect(ClowningAroundEncounter.onInit).toBeDefined();
ClowningAroundEncounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit(scene);
const config = ClowningAroundEncounter.enemyPartyConfigs[0];
expect(config.doubleBattle).toBe(true);
expect(config.trainerConfig.trainerType).toBe(TrainerType.HARLEQUIN);
expect(config.pokemonConfigs[0]).toEqual({
species: getPokemonSpecies(Species.MR_MIME),
isBoss: true,
moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC]
});
expect(config.pokemonConfigs[1]).toEqual({
species: getPokemonSpecies(Species.BLACEPHALON),
ability: expect.any(Number),
mysteryEncounterData: expect.anything(),
isBoss: true,
moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN]
});
expect(config.pokemonConfigs[1].mysteryEncounterData.types.length).toBe(2);
expect([
Abilities.STURDY,
Abilities.PICKUP,
Abilities.INTIMIDATE,
Abilities.GUTS,
Abilities.DROUGHT,
Abilities.DRIZZLE,
Abilities.SNOW_WARNING,
Abilities.SAND_STREAM,
Abilities.ELECTRIC_SURGE,
Abilities.PSYCHIC_SURGE,
Abilities.GRASSY_SURGE,
Abilities.MISTY_SURGE,
Abilities.MAGICIAN,
Abilities.SHEER_FORCE,
Abilities.PRANKSTER
]).toContain(config.pokemonConfigs[1].ability);
expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs[1].ability);
await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true);
});
describe("Option 1 - Battle the Clown", () => {
it("should have the correct properties", () => {
const option = ClowningAroundEncounter.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 double battle against the clown", async () => {
const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(2);
expect(enemyField[0].species.speciesId).toBe(Species.MR_MIME);
expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.TEETER_DANCE), new PokemonMove(Moves.ALLY_SWITCH), new PokemonMove(Moves.DAZZLING_GLEAM), new PokemonMove(Moves.PSYCHIC)]);
expect(enemyField[1].species.speciesId).toBe(Species.BLACEPHALON);
expect(enemyField[1].moveset).toEqual([new PokemonMove(Moves.TRICK), new PokemonMove(Moves.HYPNOSIS), new PokemonMove(Moves.SHADOW_BALL), new PokemonMove(Moves.MIND_BLOWN)]);
// 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(3);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.ROLE_PLAY).length).toBe(1);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TAUNT).length).toBe(2);
});
it("should let the player gain the ability after battle completion", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
const abilityToTrain = scene.currentBattle.mysteryEncounter.misc.ability;
game.onNextPrompt("PostMysteryEncounterPhase", Mode.MESSAGE, () => {
game.scene.ui.getHandler().processInput(Button.ACTION);
});
// Run to ability train option selection
const optionSelectUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler;
vi.spyOn(optionSelectUiHandler, "show");
const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler;
vi.spyOn(partyUiHandler, "show");
game.endPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
expect(scene.getCurrentPhase().constructor.name).toBe(PostMysteryEncounterPhase.name);
// Wait for Yes/No confirmation to appear
await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled());
// Select "Yes" on train ability
optionSelectUiHandler.processInput(Button.ACTION);
// Select first pokemon in party to train
await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled());
partyUiHandler.processInput(Button.ACTION);
// Click "Select" on Pokemon
partyUiHandler.processInput(Button.ACTION);
// Stop next battle before it runs
await game.phaseInterceptor.to(NewBattlePhase, false);
const leadPokemon = scene.getParty()[0];
expect(leadPokemon.mysteryEncounterData.ability).toBe(abilityToTrain);
});
});
describe("Option 2 - Remain Unprovoked", () => {
it("should have the correct properties", () => {
const option = ClowningAroundEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.2.selected`,
},
{
text: `${namespace}.option.2.selected_2`,
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.2.selected_3`,
},
],
});
});
it("should randomize held items of the Pokemon with the most items, and not the held items of other pokemon", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
// 2 Sitrus Berries on lead
scene.modifiers = [];
let itemType = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 2, itemType);
// 2 Ganlon Berries on lead
itemType = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.GANLON]).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 2, itemType);
// 5 Golden Punch on lead (ultra)
itemType = generateModifierTypeOption(scene, modifierTypes.GOLDEN_PUNCH).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 5, itemType);
// 5 Lucky Egg on lead (ultra)
itemType = generateModifierTypeOption(scene, modifierTypes.LUCKY_EGG).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 5, itemType);
// 5 Soul Dew on lead (rogue)
itemType = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 5, itemType);
// 2 Golden Egg on lead (rogue)
itemType = generateModifierTypeOption(scene, modifierTypes.GOLDEN_EGG).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 2, itemType);
// 5 Soul Dew on second party pokemon (these should not change)
itemType = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[1], 5, itemType);
await runMysteryEncounterToEnd(game, 2);
const leadItemsAfter = scene.getParty()[0].getHeldItems();
const ultraCountAfter = leadItemsAfter
.filter(m => m.type.tier === ModifierTier.ULTRA)
.reduce((a, b) => a + b.stackCount, 0);
const rogueCountAfter = leadItemsAfter
.filter(m => m.type.tier === ModifierTier.ROGUE)
.reduce((a, b) => a + b.stackCount, 0);
expect(ultraCountAfter).toBe(10);
expect(rogueCountAfter).toBe(7);
const secondItemsAfter = scene.getParty()[1].getHeldItems();
expect(secondItemsAfter.length).toBe(1);
expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW");
expect(secondItemsAfter[0].stackCount).toBe(5);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - Return the Insults", () => {
it("should have the correct properties", () => {
const option = ClowningAroundEncounter.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: [
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.3.selected`,
},
{
text: `${namespace}.option.3.selected_2`,
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.3.selected_3`,
},
],
});
});
it("should randomize the pokemon types of the party", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
// Same type moves on lead
scene.getParty()[0].moveset = [new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF)];
// Different type moves on second
scene.getParty()[1].moveset = [new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL)];
// No moves on third
scene.getParty()[2].moveset = [];
await runMysteryEncounterToEnd(game, 3);
const leadTypesAfter = scene.getParty()[0].mysteryEncounterData.types;
const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterData.types;
const thirdTypesAfter = scene.getParty()[2].mysteryEncounterData.types;
expect(leadTypesAfter.length).toBe(2);
expect(leadTypesAfter).not.toBe([Type.ICE, Type.WATER]);
expect(secondaryTypesAfter.length).toBe(2);
expect(secondaryTypesAfter.includes(Type.GRASS)).toBeTruthy();
expect(secondaryTypesAfter.includes(Type.ELECTRIC)).toBeTruthy();
expect(thirdTypesAfter.length).toBe(1);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});
async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: integer, itemType: PokemonHeldItemModifierType) {
const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier;
itemMod.stackCount = stackCount;
await scene.addModifier(itemMod, true, false, false, true);
await scene.updateModifiers(true);
}

View File

@ -23,6 +23,7 @@ import { PokemonBaseStatTotalModifier } from "#app/modifier/modifier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
const namespace = "mysteryEncounter:theStrongStuff";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -118,7 +119,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
spriteScale: 1.5,
mysteryEncounterData: new MysteryEncounterPokemonData(1.5),
nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierTypes: expect.any(Array),

View File

@ -81,8 +81,8 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.rarityBall.setScale(0.75);
this.descriptionContainer.add(this.rarityBall);
const dexProgressIndicator = this.scene.add.sprite(12, 9, "encounter_radar");
dexProgressIndicator.setScale(0.85);
const dexProgressIndicator = this.scene.add.sprite(12, 10, "encounter_radar");
dexProgressIndicator.setScale(0.80);
this.dexProgressContainer.add(dexProgressIndicator);
this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains);
this.dexProgressContainer.on("pointerover", () => {