[Balance] Safeguard to prevent Paradox Pokemon spawning in ME's
This commit is contained in:
parent
a86afa6725
commit
695965f0df
|
@ -0,0 +1,46 @@
|
|||
import { Species } from "#enums/species";
|
||||
|
||||
/**
|
||||
* A list of all {@link https://bulbapedia.bulbagarden.net/wiki/Paradox_Pok%C3%A9mon | Paradox Pokemon}, including the legendaries Miraidon and Koraidon.
|
||||
*/
|
||||
export const PARADOX_POKEMON = [
|
||||
Species.GREAT_TUSK,
|
||||
Species.SCREAM_TAIL,
|
||||
Species.BRUTE_BONNET,
|
||||
Species.FLUTTER_MANE,
|
||||
Species.SLITHER_WING,
|
||||
Species.SANDY_SHOCKS,
|
||||
Species.ROARING_MOON,
|
||||
Species.WALKING_WAKE,
|
||||
Species.GOUGING_FIRE,
|
||||
Species.RAGING_BOLT,
|
||||
Species.IRON_TREADS,
|
||||
Species.IRON_BUNDLE,
|
||||
Species.IRON_HANDS,
|
||||
Species.IRON_JUGULIS,
|
||||
Species.IRON_MOTH,
|
||||
Species.IRON_THORNS,
|
||||
Species.IRON_VALIANT,
|
||||
Species.IRON_LEAVES,
|
||||
Species.IRON_BOULDER,
|
||||
Species.IRON_CROWN,
|
||||
Species.KORAIDON,
|
||||
Species.MIRAIDON,
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of all {@link https://bulbapedia.bulbagarden.net/wiki/Ultra_Beast | Ultra Beasts}, NOT including legendaries such as Necrozma.
|
||||
*/
|
||||
export const ULTRA_BEASTS = [
|
||||
Species.NIHILEGO,
|
||||
Species.BUZZWOLE,
|
||||
Species.PHEROMOSA,
|
||||
Species.XURKITREE,
|
||||
Species.CELESTEELA,
|
||||
Species.KARTANA,
|
||||
Species.GUZZLORD,
|
||||
Species.POIPOLE,
|
||||
Species.NAGANADEL,
|
||||
Species.STAKATAKA,
|
||||
Species.BLACEPHALON,
|
||||
];
|
|
@ -9,7 +9,7 @@ import { EnemyPokemon } from "#app/field/pokemon";
|
|||
import { PokeballType } from "#enums/pokeball";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { IntegerHolder, randSeedInt } from "#app/utils";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
||||
import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
|
@ -19,6 +19,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
|
|||
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
|
||||
import { SummonPhase } from "#app/phases/summon-phase";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
|
||||
import { PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/safariZone";
|
||||
|
@ -261,7 +262,7 @@ async function summonSafariPokemon(scene: BattleScene) {
|
|||
let enemySpecies;
|
||||
let pokemon;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false));
|
||||
enemySpecies = getSafariSpeciesSpawn();
|
||||
const level = scene.currentBattle.getLevelForWave();
|
||||
enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode));
|
||||
pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false);
|
||||
|
@ -526,3 +527,10 @@ async function doEndTurn(scene: BattleScene, cursorIndex: number) {
|
|||
initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
|
||||
*/
|
||||
export function getSafariSpeciesSpawn(): PokemonSpecies {
|
||||
return getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], PARADOX_POKEMON, undefined, false, false, false));
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import BattleScene from "#app/battle-scene";
|
|||
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
|
||||
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
||||
import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { speciesStarterCosts } from "#app/data/balance/starters";
|
||||
import { Species } from "#enums/species";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
|
@ -17,6 +17,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
|||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounters/thePokemonSalesman";
|
||||
|
@ -60,12 +61,12 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
|
|||
.withOnInit((scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
|
||||
let species = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false));
|
||||
let species = getSalesmanSpeciesOffer();
|
||||
let tries = 0;
|
||||
|
||||
// Reroll any species that don't have HAs
|
||||
while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) {
|
||||
species = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false));
|
||||
species = getSalesmanSpeciesOffer();
|
||||
tries++;
|
||||
}
|
||||
|
||||
|
@ -164,3 +165,10 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
|
|||
}
|
||||
)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
|
||||
*/
|
||||
export function getSalesmanSpeciesOffer(): PokemonSpecies {
|
||||
return getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], PARADOX_POKEMON, undefined, false, false, false));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
|
||||
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
|
||||
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
|
||||
import { getSafariSpeciesSpawn, SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter";
|
||||
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
|
||||
|
||||
const namespace = "mysteryEncounters/safariZone";
|
||||
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
|
||||
const defaultBiome = Biome.SWAMP;
|
||||
const defaultWave = 45;
|
||||
|
||||
describe("Safari Zone - Mystery Encounter", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let scene: BattleScene;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
game = new GameManager(phaserGame);
|
||||
scene = game.scene;
|
||||
game.override.mysteryEncounterChance(100);
|
||||
game.override.startingWave(defaultWave);
|
||||
game.override.startingBiome(defaultBiome);
|
||||
game.override.disableTrainerWaves();
|
||||
|
||||
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
|
||||
[ Biome.VOLCANO, [ MysteryEncounterType.FIGHT_OR_FLIGHT ]],
|
||||
[ Biome.FOREST, [ MysteryEncounterType.SAFARI_ZONE ]],
|
||||
[ Biome.SWAMP, [ MysteryEncounterType.SAFARI_ZONE ]],
|
||||
[ Biome.JUNGLE, [ MysteryEncounterType.SAFARI_ZONE ]],
|
||||
]);
|
||||
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.SAFARI_ZONE, defaultParty);
|
||||
|
||||
expect(SafariZoneEncounter.encounterType).toBe(MysteryEncounterType.SAFARI_ZONE);
|
||||
expect(SafariZoneEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT);
|
||||
expect(SafariZoneEncounter.dialogue).toBeDefined();
|
||||
expect(SafariZoneEncounter.dialogue.intro).toStrictEqual([
|
||||
{ text: `${namespace}:intro` },
|
||||
]);
|
||||
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`);
|
||||
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
|
||||
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`);
|
||||
expect(SafariZoneEncounter.options.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should not spawn outside of the forest, swamp, or jungle biomes", async () => {
|
||||
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
|
||||
game.override.startingBiome(Biome.VOLCANO);
|
||||
await game.runToMysteryEncounter();
|
||||
|
||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.SAFARI_ZONE);
|
||||
});
|
||||
|
||||
it("should initialize fully", async () => {
|
||||
initSceneWithoutEncounterPhase(scene, defaultParty);
|
||||
scene.currentBattle.mysteryEncounter = new MysteryEncounter(SafariZoneEncounter);
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
scene.currentBattle.waveIndex = defaultWave;
|
||||
|
||||
const { onInit } = encounter;
|
||||
|
||||
expect(encounter.onInit).toBeDefined();
|
||||
|
||||
encounter.populateDialogueTokensFromRequirements(scene);
|
||||
const onInitResult = onInit!(scene);
|
||||
expect(onInitResult).toBe(true);
|
||||
});
|
||||
|
||||
describe("Option 1 - Enter", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option = SafariZoneEncounter.options[0];
|
||||
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
|
||||
expect(option.dialogue).toBeDefined();
|
||||
expect(option.dialogue).toStrictEqual({
|
||||
buttonLabel: `${namespace}:option.1.label`,
|
||||
buttonTooltip: `${namespace}:option.1.tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}:option.1.selected`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT be selectable if the player doesn't have enough money", async () => {
|
||||
game.scene.money = 0;
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
|
||||
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
|
||||
|
||||
const encounterPhase = scene.getCurrentPhase();
|
||||
expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name);
|
||||
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase;
|
||||
vi.spyOn(mysteryEncounterPhase, "continueEncounter");
|
||||
vi.spyOn(mysteryEncounterPhase, "handleOptionSelect");
|
||||
vi.spyOn(scene.ui, "playError");
|
||||
|
||||
await runSelectMysteryEncounterOption(game, 1);
|
||||
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name);
|
||||
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
|
||||
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
|
||||
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not spawn any Paradox Pokemon", async () => {
|
||||
const NUM_ROLLS = 2000; // As long as this is greater than total number of species, this should cover all possible RNG rolls
|
||||
let rngSweepProgress = 0; // Will simulate full range of RNG rolls by steadily increasing from 0 to 1
|
||||
|
||||
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
|
||||
return rngSweepProgress * (max - min) + min;
|
||||
});
|
||||
vi.spyOn(Phaser.Math.RND, "shuffle").mockImplementation((arr: any[]) => arr);
|
||||
|
||||
for (let i = 0; i < NUM_ROLLS; i++) {
|
||||
rngSweepProgress = (2 * i + 1) / (2 * NUM_ROLLS);
|
||||
const simSpecies = getSafariSpeciesSpawn().speciesId;
|
||||
expect(PARADOX_POKEMON).not.toContain(simSpecies);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Tests for player actions inside the Safari Zone (Pokeball, Mud, Bait, Flee)
|
||||
});
|
||||
|
||||
describe("Option 2 - Leave", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option = SafariZoneEncounter.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: [
|
||||
{
|
||||
text: `${namespace}:option.2.selected`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should leave encounter without battle", async () => {
|
||||
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 2);
|
||||
|
||||
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,11 +9,12 @@ import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test
|
|||
import BattleScene from "#app/battle-scene";
|
||||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
|
||||
import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter";
|
||||
import { getSalesmanSpeciesOffer, ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
|
||||
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
|
||||
import { PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
|
||||
|
||||
const namespace = "mysteryEncounters/thePokemonSalesman";
|
||||
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
|
||||
|
@ -172,6 +173,22 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
|
|||
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not spawn any Paradox Pokemon", async () => {
|
||||
const NUM_ROLLS = 2000; // As long as this is greater than total number of species, this should cover all possible RNG rolls
|
||||
let rngSweepProgress = 0; // Will simulate full range of RNG rolls by steadily increasing from 0 to 1
|
||||
|
||||
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
|
||||
return rngSweepProgress * (max - min) + min;
|
||||
});
|
||||
vi.spyOn(Phaser.Math.RND, "shuffle").mockImplementation((arr: any[]) => arr);
|
||||
|
||||
for (let i = 0; i < NUM_ROLLS; i++) {
|
||||
rngSweepProgress = (2 * i + 1) / (2 * NUM_ROLLS);
|
||||
const simSpecies = getSalesmanSpeciesOffer().speciesId;
|
||||
expect(PARADOX_POKEMON).not.toContain(simSpecies);
|
||||
}
|
||||
});
|
||||
|
||||
it("should leave encounter without battle", async () => {
|
||||
scene.money = 20000;
|
||||
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||
|
|
Loading…
Reference in New Issue