diff --git a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts index 8ba7b08feaa..9c8ee2ac91c 100644 --- a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts @@ -94,7 +94,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = encounter.setDialogueToken("purchasePokemon", pokemon.name); encounter.setDialogueToken("price", price.toString()); encounter.misc = { - money: price, + price: price, pokemon: pokemon }; @@ -118,11 +118,11 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; - const cost = encounter.misc.money; + const price = encounter.misc.price; const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon; // Update money - updatePlayerMoney(scene, -cost, true, false); + updatePlayerMoney(scene, -price, true, false); // Show dialogue await showEncounterDialogue(scene, `${namespace}:option:1:selected_dialogue`, `${namespace}:speaker`); diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 37dcca3e412..9178747500f 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -316,6 +316,7 @@ export function initCustomMovesForEncounter(scene: BattleScene, moves: Moves | M * @param scene - Battle Scene * @param changeValue * @param playSound + * @param showMessage */ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true, showMessage: boolean = true) { scene.money = Math.min(Math.max(scene.money + changeValue, 0), Number.MAX_SAFE_INTEGER); diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 620265c0edb..d97c267b095 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -173,6 +173,9 @@ 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) { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 3b62e1dc983..1ce8ed87156 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -273,8 +273,6 @@ export class LoadingScene extends SceneBase { this.loadAtlas("xbox", "inputs"); this.loadAtlas("keyboard", "inputs"); - this.loadAtlas("encounter_radar", "mystery-encounters"); - this.loadSe("select"); this.loadSe("menu_open"); this.loadSe("hit"); diff --git a/src/overrides.ts b/src/overrides.ts index e2d3c73f76d..e19a5bf20dd 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.POKEMON_SALESMAN; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; /** * MODIFIER / ITEM OVERRIDES diff --git a/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts new file mode 100644 index 00000000000..c3045b599eb --- /dev/null +++ b/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts @@ -0,0 +1,171 @@ +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 * 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 BattleScene from "#app/battle-scene"; +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-encounter"; + +const namespace = "mysteryEncounter:pokemonSalesman"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Pokemon Salesman - 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); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.POKEMON_SALESMAN]); + }); + 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.POKEMON_SALESMAN, defaultParty); + + expect(PokemonSalesmanEncounter.encounterType).toBe(MysteryEncounterType.POKEMON_SALESMAN); + expect(PokemonSalesmanEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(PokemonSalesmanEncounter.dialogue).toBeDefined(); + expect(PokemonSalesmanEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}:intro` }, + { speaker: "mysteryEncounter:pokemonSalesman:speaker", text: "mysteryEncounter:pokemonSalesman:intro_dialogue" } + ]); + expect(PokemonSalesmanEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); + expect(PokemonSalesmanEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); + expect(PokemonSalesmanEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`); + expect(PokemonSalesmanEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.POKEMON_SALESMAN); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.POKEMON_SALESMAN); + }); + + 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: PokemonSalesmanEncounter } as Battle); + + const { onInit } = PokemonSalesmanEncounter; + + expect(PokemonSalesmanEncounter.onInit).toBeDefined(); + + const onInitResult = onInit(scene); + + expect(PokemonSalesmanEncounter.dialogueTokens?.purchasePokemon).toBeDefined(); + expect(PokemonSalesmanEncounter.dialogueTokens?.price).toBeDefined(); + expect(PokemonSalesmanEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); + expect(PokemonSalesmanEncounter.misc?.price?.toString()).toBe(PokemonSalesmanEncounter.dialogueTokens?.price); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Purchase the pokemon", () => { + it("should have the correct properties", () => { + const option1 = PokemonSalesmanEncounter.options[0]; + expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT_OR_SPECIAL); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + selected: [ + { + text: `${namespace}:option:1:selected_message`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); + await runSelectMysteryEncounterOption(game, 1); + + const price = scene.currentBattle.mysteryEncounter.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); + expect(scene.money).toBe(initialMoney - price); + }); + + it("Should add the Pokemon to the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); + + const initialPartySize = scene.getParty().length; + const pokemonName = scene.currentBattle.mysteryEncounter.misc.pokemon.name; + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getParty().length).toBe(initialPartySize + 1); + expect(scene.getParty().find(p => p.name === pokemonName) instanceof PlayerPokemon).toBeTruthy(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); + await runSelectMysteryEncounterOption(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); + await runSelectMysteryEncounterOption(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); 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 index ed15380b8c9..b57683d3165 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -22,6 +22,7 @@ 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"; +import { PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; const namespace = "mysteryEncounter:theStrongStuff"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -153,7 +154,9 @@ describe("The Strong Stuff - Mystery Encounter", () => { await runSelectMysteryEncounterOption(game, 1); const bstsAfter = scene.getParty().map(p => { - return p.getSpeciesForm().getBaseStatTotal(); + const baseStats = p.getSpeciesForm().baseStats.slice(0); + scene.applyModifiers(PokemonBaseStatTotalModifier, true, p, baseStats); + return baseStats.reduce((a, b) => a + b); }); expect(bstsAfter[0]).toEqual(bstsPrior[0] - 20 * 6); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index cdc7eda180f..a336af41b6b 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -298,7 +298,7 @@ describe("Mystery Encounter Utils", () => { const spy = vi.spyOn(game.scene.ui, "showDialogue"); showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue"); - expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, undefined, 0, 0); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0); }); }); diff --git a/src/test/utils/mocks/mocksContainer/mockText.ts b/src/test/utils/mocks/mocksContainer/mockText.ts index 2e6ed67f21f..2509c38b182 100644 --- a/src/test/utils/mocks/mocksContainer/mockText.ts +++ b/src/test/utils/mocks/mocksContainer/mockText.ts @@ -248,6 +248,14 @@ export default class MockText { }; } + disableInteractive() { + // Disables interaction with this Game Object. + } + + clearTint() { + // Clears tint on this Game Object. + } + add(obj) { // Adds a child to this Game Object. this.list.push(obj);