add part timer and dancing lessons encounters

This commit is contained in:
ImperialSympathizer 2024-08-11 22:41:22 -04:00
parent ed12d18205
commit f90d8b0575
17 changed files with 1991 additions and 51 deletions

View File

@ -0,0 +1,951 @@
{
"id": 686,
"graphic": "PRAS- Dragon Dance",
"frames": [
[
{
"x": 4,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 12,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -12,
"y": -0.5,
"zoomX": 100,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": 12,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 16,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -16,
"y": -0.5,
"zoomX": 100,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": 24,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 20,
"y": 0,
"zoomX": 108,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -20,
"y": -0.5,
"zoomX": 108,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": 32,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 24,
"y": 0,
"zoomX": 108,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -24,
"y": -0.5,
"zoomX": 108,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": 36,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 28,
"y": 0,
"zoomX": 108,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -28,
"y": -0.5,
"zoomX": 108,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": 36,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 36,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 32,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 24,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 12,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 4,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": -4,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 12,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -12,
"y": -0.5,
"zoomX": 100,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": -12,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 16,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -16,
"y": -0.5,
"zoomX": 100,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": -24,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 20,
"y": 0,
"zoomX": 108,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -20,
"y": -0.5,
"zoomX": 108,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": -32,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 24,
"y": 0,
"zoomX": 108,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -24,
"y": -0.5,
"zoomX": 108,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 155,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": -36,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
},
{
"x": 28,
"y": 0,
"zoomX": 108,
"zoomY": 100,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
},
{
"x": -28,
"y": -0.5,
"zoomX": 108,
"zoomY": 100,
"mirror": true,
"visible": true,
"blendType": 1,
"target": 2,
"graphicFrame": 0,
"opacity": 70,
"tone": [
0,
0,
0,
255
],
"priority": 1,
"focus": 3
}
],
[
{
"x": -36,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": -36,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": -32,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": -24,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": -12,
"y": -12,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": -4,
"y": -8,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
],
[
{
"x": 0,
"y": 0,
"zoomX": 100,
"zoomY": 100,
"visible": true,
"target": 0,
"graphicFrame": 0,
"opacity": 255,
"locked": true,
"priority": 1,
"focus": 2
}
]
],
"frameTimedEvents": {
"0": [
{
"frameIndex": 0,
"resourceName": "PRSFX- Attract.wav",
"volume": 100,
"pitch": 100,
"eventType": "AnimTimedSoundEvent"
}
],
"1": [
{
"frameIndex": 0,
"resourceName": "PRSFX- Ally Switch.wav",
"volume": 80,
"pitch": 100,
"eventType": "AnimTimedSoundEvent"
}
]
},
"position": 4,
"hue": 0
}

View File

@ -66,7 +66,7 @@ import { Species } from "#enums/species";
import { UiTheme } from "#enums/ui-theme";
import { TimedEventManager } from "#app/timed-event-manager.js";
import i18next from "i18next";
import {TrainerType} from "#enums/trainer-type";
import { TrainerType } from "#enums/trainer-type";
import IMysteryEncounter from "./data/mystery-encounters/mystery-encounter";
import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters";
import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data";

View File

@ -7,6 +7,7 @@ import { BattlerIndex } from "../battle";
import { Element } from "json-stable-stringify";
import { Moves } from "#enums/moves";
import { isNullOrUndefined } from "../utils";
import Phaser from "phaser";
//import fs from 'vite-plugin-fs/browser';
export enum AnimFrameTarget {
@ -111,7 +112,8 @@ export enum CommonAnim {
export enum EncounterAnim {
MAGMA_BG,
MAGMA_SPOUT,
SMOKESCREEN
SMOKESCREEN,
DANCE
}
export class AnimConfig {
@ -1264,11 +1266,13 @@ export class MoveChargeAnim extends MoveAnim {
export class EncounterBattleAnim extends BattleAnim {
public encounterAnim: EncounterAnim;
public oppAnim: boolean;
constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon) {
super(user, target || user);
constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon, oppAnim?: boolean) {
super(user, target || user, true);
this.encounterAnim = encounterAnim;
this.oppAnim = oppAnim ?? false;
}
getAnim(): AnimConfig {
@ -1276,7 +1280,7 @@ export class EncounterBattleAnim extends BattleAnim {
}
isOppAnim(): boolean {
return false;
return this.oppAnim;
}
}

View File

@ -254,13 +254,12 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter =
};
setEncounterRewards(scene, { fillRemaining: true }, null, givePartyPokemonReviverSeeds);
encounter.startOfBattleEffects.push(
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY],
move: new PokemonMove(Moves.STUFF_CHEEKS),
ignorePp: true
});
encounter.startOfBattleEffects.push({
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY],
move: new PokemonMove(Moves.STUFF_CHEEKS),
ignorePp: true
});
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);

View File

@ -0,0 +1,279 @@
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves";
import { TrainerSlot } from "#app/data/trainer-config";
import PokemonData from "#app/system/pokemon-data";
import { Biome } from "#enums/biome";
import { EncounterAnim, EncounterBattleAnim } from "#app/data/battle-anims";
import { BattlerTagType } from "#enums/battler-tag-type";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { LearnMovePhase, StatChangePhase } from "#app/phases";
import { BattleStat } from "#app/data/battle-stat";
import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { BattlerIndex } from "#app/battle";
import { catchPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { PokeballType } from "#enums/pokeball";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounter:dancingLessons";
// Fire form
const BAILE_STYLE_BIOMES = [
Biome.VOLCANO,
Biome.BEACH,
Biome.ISLAND,
Biome.WASTELAND,
Biome.MOUNTAIN,
Biome.BADLANDS,
Biome.DESERT
];
// Electric form
const POM_POM_STYLE_BIOMES = [
Biome.CONSTRUCTION_SITE,
Biome.POWER_PLANT,
Biome.FACTORY,
Biome.LABORATORY,
Biome.SLUM,
Biome.METROPOLIS,
Biome.DOJO
];
// Psychic form
const PAU_STYLE_BIOMES = [
Biome.JUNGLE,
Biome.FAIRY_CAVE,
Biome.MEADOW,
Biome.PLAINS,
Biome.GRASS,
Biome.TALL_GRASS,
Biome.FOREST
];
// Ghost form
const SENSU_STYLE_BIOMES = [
Biome.RUINS,
Biome.SWAMP,
Biome.CAVE,
Biome.ABYSS,
Biome.GRAVEYARD,
Biome.LAKE,
Biome.TEMPLE
];
/**
* Dancing Lessons encounter.
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/130 | GitHub Issue #130}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const DancingLessonsEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DANCING_LESSONS)
.withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(10, 180)
.withIntroSpriteConfigs([]) // Uses a real Pokemon sprite instead of ME Intro Visuals
.withAnimations(EncounterAnim.DANCE)
.withHideWildIntroMessage(true)
.withAutoHideIntroVisuals(false)
.withCatchAllowed(true)
.withOnVisualsStart((scene: BattleScene) => {
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon(), scene.getPlayerPokemon());
danceAnim.play(scene);
return true;
})
.withIntroDialogue([
{
text: `${namespace}.intro`,
}
])
.withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`)
.withQuery(`${namespace}.query`)
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
const species = getPokemonSpecies(Species.ORICORIO);
const enemyPokemon = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels[0], TrainerSlot.NONE, false);
if (!enemyPokemon.moveset.some(m => m.getMove().id === Moves.REVELATION_DANCE)) {
if (enemyPokemon.moveset.length < 4) {
enemyPokemon.moveset.push(new PokemonMove(Moves.REVELATION_DANCE));
} else {
enemyPokemon.moveset[0] = new PokemonMove(Moves.REVELATION_DANCE);
}
}
// Set the form index based on the biome
// Defaults to Baile style if somehow nothing matches
const currentBiome = scene.arena.biomeType;
if (BAILE_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 0;
} else if (POM_POM_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 1;
} else if (PAU_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 2;
} else if (SENSU_STYLE_BIOMES.includes(currentBiome)) {
enemyPokemon.formIndex = 3;
} else {
enemyPokemon.formIndex = 0;
}
const oricorioData = new PokemonData(enemyPokemon);
// Adds a real Pokemon sprite to the field (required for the animation)
scene.currentBattle.enemyParty[0] = enemyPokemon;
scene.field.add(enemyPokemon);
const config: EnemyPartyConfig = {
levelAdditiveMultiplier: 1,
pokemonConfigs: [{
species: species,
dataSource: oricorioData,
isBoss: true,
// Gets +1 to all stats on battle start
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(pokemon.scene, `${namespace}.option.1.boss_enraged`);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1));
}
}],
};
encounter.enemyPartyConfigs = [config];
encounter.misc = {
oricorioData
};
return true;
})
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.1.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
// Pick battle
const encounter = scene.currentBattle.mysteryEncounter;
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
encounter.startOfBattleEffects.push({
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER],
move: new PokemonMove(Moves.REVELATION_DANCE),
ignorePp: true
});
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
})
.build()
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene) => {
// Learn its Dance
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
scene.unshiftPhase(new LearnMovePhase(scene, scene.getParty().indexOf(pokemon), Moves.REVELATION_DANCE));
// Play animation again to "learn" the dance
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon(), scene.getPlayerPokemon());
danceAnim.play(scene);
};
return selectPokemonForOption(scene, onPokemonSelected);
})
.withOptionPhase(async (scene: BattleScene) => {
// Learn its Dance
leaveEncounterWithoutBattle(scene, true);
})
.build()
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
disabledButtonTooltip: `${namespace}.option.3.tooltip`,
secondOptionPrompt: `${namespace}.option.3.select_prompt`,
selected: [
{
text: `${namespace}.option.3.selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene) => {
// Open menu for selecting pokemon with a Dancing move
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Return the options for nature selection
return pokemon.moveset
.filter(move => DANCING_MOVES.includes(move.getMove().id))
.map((move: PokemonMove) => {
const option: OptionSelectItem = {
label: move.getName(),
handler: () => {
// Pokemon and second option selected
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
encounter.setDialogueToken("selectedMove", move.getName());
return true;
},
};
return option;
});
};
// Only Pokemon that have a Dancing move can be selected
const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected
const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => {
// Show the Oricorio a dance, and recruit it
const oricorio = scene.currentBattle.mysteryEncounter.misc.oricorioData.toPokemon(scene);
oricorio.passive = true;
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true);
})
.build()
)
.build();

View File

@ -0,0 +1,327 @@
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MoveRequirement } from "../mystery-encounter-requirements";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Stat } from "#enums/stat";
import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { getEncounterText, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import i18next from "i18next";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:partTimer";
/**
* Part Timer encounter.
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/82 | GitHub Issue #82}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const PartTimerEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.PART_TIMER)
.withEncounterTier(MysteryEncounterTier.COMMON)
.withSceneWaveRangeRequirement(10, 180)
.withIntroSpriteConfigs([
{
spriteKey: "worker_f",
fileRoot: "trainer",
hasShadow: true,
x: -20
},
{
spriteKey: "training_gear",
fileRoot: "mystery-encounters",
hasShadow: true,
y: 6,
x: 20,
yShadow: -2
}
])
.withAutoHideIntroVisuals(false)
.withIntroDialogue([
{
text: `${namespace}.intro`,
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`,
},
])
.withOnInit((scene: BattleScene) => {
// Load sfx
scene.loadSe("PRSFX- Horn Drill1", "battle_anims", "PRSFX- Horn Drill1.wav");
scene.loadSe("PRSFX- Horn Drill3", "battle_anims", "PRSFX- Horn Drill3.wav");
scene.loadSe("PRSFX- Guillotine2", "battle_anims", "PRSFX- Guillotine2.wav");
scene.loadSe("PRSFX- Heavy Slam2", "battle_anims", "PRSFX- Heavy Slam2.wav");
scene.loadSe("PRSFX- Agility", "battle_anims", "PRSFX- Agility.wav");
scene.loadSe("PRSFX- Extremespeed1", "battle_anims", "PRSFX- Extremespeed1.wav");
scene.loadSe("PRSFX- Accelerock1", "battle_anims", "PRSFX- Accelerock1.wav");
scene.loadSe("PRSFX- Captivate", "battle_anims", "PRSFX- Captivate.wav");
scene.loadSe("PRSFX- Attract2", "battle_anims", "PRSFX- Attract2.wav");
scene.loadSe("PRSFX- Aurora Veil2", "battle_anims", "PRSFX- Aurora Veil2.wav");
return true;
})
.withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`)
.withQuery(`${namespace}.query`)
.withOption(new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.1.selected`
}
]
})
.withPreOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
// Calculate the "baseline" stat value (100 base stat, 31 IVs, neutral nature, same level as pokemon) to compare
// Resulting money is 2.5 * (% difference from baseline), with minimum of 1 and maximum of 4.
// Calculation from Pokemon.calculateStats
const baselineValue = Math.floor(((2 * 100 + 31) * pokemon.level) * 0.01) + 5;
const percentDiff = (pokemon.getStat(Stat.SPD) - baselineValue) / baselineValue;
const moneyMultiplier = Math.min(Math.max(2.5 * (1+ percentDiff), 1), 4);
encounter.misc = {
moneyMultiplier
};
// Reduce all PP to 2 (if they started at greater than 2)
pokemon.moveset.forEach(move => {
const newPpUsed = move.getMovePp() - 2;
move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed;
});
// Hide intro visuals
transitionMysteryEncounterIntroVisuals(scene, true, false);
// Play sfx for "working"
doDeliverySfx(scene);
};
// Only Pokemon non-KOd pokemon can be selected
const selectableFilter = (pokemon: Pokemon) => {
if (!pokemon.isAllowedInBattle()) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => {
// Pick Deliveries
// Bring visuals back in
await transitionMysteryEncounterIntroVisuals(scene, false, false);
const moneyMultiplier = scene.currentBattle.mysteryEncounter.misc.moneyMultiplier;
// Give money and do dialogue
if (moneyMultiplier > 2.5) {
await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`);
} else {
await showEncounterDialogue(scene, `${namespace}.job_complete_bad`, `${namespace}.speaker`);
}
const moneyChange = scene.getWaveMoneyAmount(moneyMultiplier);
updatePlayerMoney(scene, moneyChange, true, false);
await showEncounterText(scene, i18next.t("mysteryEncounter:receive_money", { amount: moneyChange }));
await showEncounterText(scene, `${namespace}.pokemon_tired`);
setEncounterRewards(scene, { fillRemaining: true });
leaveEncounterWithoutBattle(scene);
})
.build()
)
.withOption(new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`
}
]
})
.withPreOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
// Calculate the "baseline" stat value (100 base stat, 31 IVs, neutral nature, same level as pokemon) to compare
// Resulting money is 2.5 * (% difference from baseline), with minimum of 1 and maximum of 4.
// Calculation from Pokemon.calculateStats
const baselineHp = Math.floor(((2 * 80 + 31) * pokemon.level) * 0.01) + pokemon.level + 10;
const baselineAtkDef = Math.floor(((2 * 80 + 31) * pokemon.level) * 0.01) + 5;
const baselineValue = baselineHp + 1.5 * (baselineAtkDef * 2);
const strongestValue = pokemon.getStat(Stat.HP) + 1.5 * (pokemon.getStat(Stat.ATK) + pokemon.getStat(Stat.DEF));
const percentDiff = (strongestValue - baselineValue) / baselineValue;
const moneyMultiplier = Math.min(Math.max(2.5 * (1+ percentDiff), 1), 4);
encounter.misc = {
moneyMultiplier
};
// Reduce all PP to 2 (if they started at greater than 2)
pokemon.moveset.forEach(move => {
const newPpUsed = move.getMovePp() - 2;
move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed;
});
// Hide intro visuals
transitionMysteryEncounterIntroVisuals(scene, true, false);
// Play sfx for "working"
doStrongWorkSfx(scene);
};
// Only Pokemon non-KOd pokemon can be selected
const selectableFilter = (pokemon: Pokemon) => {
if (!pokemon.isAllowedInBattle()) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => {
// Pick Move Warehouse items
// Bring visuals back in
await transitionMysteryEncounterIntroVisuals(scene, false, false);
const moneyMultiplier = scene.currentBattle.mysteryEncounter.misc.moneyMultiplier;
// Give money and do dialogue
if (moneyMultiplier > 2.5) {
await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`);
} else {
await showEncounterDialogue(scene, `${namespace}.job_complete_bad`, `${namespace}.speaker`);
}
const moneyChange = scene.getWaveMoneyAmount(moneyMultiplier);
updatePlayerMoney(scene, moneyChange, true, false);
await showEncounterText(scene, i18next.t("mysteryEncounter:receive_money", { amount: moneyChange }));
await showEncounterText(scene, `${namespace}.pokemon_tired`);
setEncounterRewards(scene, { fillRemaining: true });
leaveEncounterWithoutBattle(scene);
})
.build()
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.3.selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
const selectedPokemon = encounter.selectedOption.primaryPokemon;
encounter.setDialogueToken("selectedPokemon", selectedPokemon.getNameToRender());
// Reduce all PP to 2 (if they started at greater than 2)
selectedPokemon.moveset.forEach(move => {
const newPpUsed = move.getMovePp() - 2;
move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed;
});
// Hide intro visuals
transitionMysteryEncounterIntroVisuals(scene, true, false);
// Play sfx for "working"
doSalesSfx(scene);
return true;
})
.withOptionPhase(async (scene: BattleScene) => {
// Assist with Sales
// Bring visuals back in
await transitionMysteryEncounterIntroVisuals(scene, false, false);
// Give money and do dialogue
await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`);
const moneyChange = scene.getWaveMoneyAmount(2.5);
updatePlayerMoney(scene, moneyChange, true, false);
await showEncounterText(scene, i18next.t("mysteryEncounter:receive_money", { amount: moneyChange }));
await showEncounterText(scene, `${namespace}.pokemon_tired`);
setEncounterRewards(scene, { fillRemaining: true });
leaveEncounterWithoutBattle(scene);
})
.build()
)
.withOutroDialogue([
{
speaker: `${namespace}.speaker`,
text: `${namespace}.outro`,
}
])
.build();
function doStrongWorkSfx(scene: BattleScene) {
scene.playSound("PRSFX- Horn Drill1");
scene.playSound("PRSFX- Horn Drill1");
scene.time.delayedCall(1000, () => {
scene.playSound("PRSFX- Guillotine2");
});
scene.time.delayedCall(2000, () => {
scene.playSound("PRSFX- Heavy Slam2");
});
scene.time.delayedCall(2500, () => {
scene.playSound("PRSFX- Guillotine2");
});
}
function doDeliverySfx(scene: BattleScene) {
scene.playSound("PRSFX- Accelerock1");
scene.time.delayedCall(1500, () => {
scene.playSound("PRSFX- Extremespeed1");
});
scene.time.delayedCall(2000, () => {
scene.playSound("PRSFX- Extremespeed1");
});
scene.time.delayedCall(2250, () => {
scene.playSound("PRSFX- Agility");
});
}
function doSalesSfx(scene: BattleScene) {
scene.playSound("PRSFX- Captivate");
scene.time.delayedCall(1500, () => {
scene.playSound("PRSFX- Attract2");
});
scene.time.delayedCall(2000, () => {
scene.playSound("PRSFX- Aurora Veil2");
});
scene.time.delayedCall(3000, () => {
scene.playSound("PRSFX- Attract2");
});
}

View File

@ -22,6 +22,8 @@ import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/
import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter";
import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter";
import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter";
import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter";
import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter";
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -135,7 +137,8 @@ export const allMysteryEncounters: { [encounterType: number]: IMysteryEncounter
const extremeBiomeEncounters: MysteryEncounterType[] = [];
const nonExtremeBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.FIELD_TRIP
MysteryEncounterType.FIELD_TRIP,
MysteryEncounterType.DANCING_LESSONS, // Is also in BADLANDS, DESERT, VOLCANO, WASTELAND, ABYSS
];
const humanTransitableBiomeEncounters: MysteryEncounterType[] = [
@ -146,7 +149,8 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [
];
const civilizationBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.DEPARTMENT_STORE_SALE
MysteryEncounterType.DEPARTMENT_STORE_SALE,
MysteryEncounterType.PART_TIMER
];
/**
@ -200,23 +204,32 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
[Biome.LAKE, []],
[Biome.SEABED, []],
[Biome.MOUNTAIN, []],
[Biome.BADLANDS, []],
[Biome.BADLANDS, [
MysteryEncounterType.DANCING_LESSONS
]],
[Biome.CAVE, [
MysteryEncounterType.THE_STRONG_STUFF
]],
[Biome.DESERT, []],
[Biome.DESERT, [
MysteryEncounterType.DANCING_LESSONS
]],
[Biome.ICE_CAVE, []],
[Biome.MEADOW, []],
[Biome.POWER_PLANT, []],
[Biome.VOLCANO, [
MysteryEncounterType.FIERY_FALLOUT
MysteryEncounterType.FIERY_FALLOUT,
MysteryEncounterType.DANCING_LESSONS
]],
[Biome.GRAVEYARD, []],
[Biome.DOJO, []],
[Biome.FACTORY, []],
[Biome.RUINS, []],
[Biome.WASTELAND, []],
[Biome.ABYSS, []],
[Biome.WASTELAND, [
MysteryEncounterType.DANCING_LESSONS
]],
[Biome.ABYSS, [
MysteryEncounterType.DANCING_LESSONS
]],
[Biome.SPACE, []],
[Biome.CONSTRUCTION_SITE, []],
[Biome.JUNGLE, [
@ -252,6 +265,8 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter;
allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter;
allMysteryEncounters[MysteryEncounterType.CLOWNING_AROUND] = ClowningAroundEncounter;
allMysteryEncounters[MysteryEncounterType.PART_TIMER] = PartTimerEncounter;
allMysteryEncounters[MysteryEncounterType.DANCING_LESSONS] = DancingLessonsEncounter;
// Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => {

View File

@ -10,6 +10,36 @@ export const STEALING_MOVES = [
Moves.SWITCHEROO
];
export const CHARMING_MOVES = [
Moves.CHARM,
Moves.FLATTER,
Moves.DRAGON_CHEER,
Moves.ALLURING_VOICE,
Moves.ATTRACT,
Moves.SWEET_SCENT,
Moves.CAPTIVATE,
Moves.AROMATIC_MIST
];
/**
* Moves for the Dancer ability
*/
export const DANCING_MOVES = [
Moves.AQUA_STEP,
Moves.CLANGOROUS_SOUL,
Moves.DRAGON_DANCE,
Moves.FEATHER_DANCE,
Moves.FIERY_DANCE,
Moves.LUNAR_DANCE,
Moves.PETAL_DANCE,
Moves.REVELATION_DANCE,
Moves.QUIVER_DANCE,
Moves.SWORDS_DANCE,
Moves.TEETER_DANCE,
Moves.VICTORY_DANCE,
Moves.KNOCK_OFF
];
export const DISTRACTION_MOVES = [
Moves.FAKE_OUT,
Moves.FOLLOW_ME,

View File

@ -423,7 +423,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
return true;
},
onHover: () => {
scene.ui.showText("Return to encounter option select.");
scene.ui.showText(i18next.t("mysteryEncounter:cancel_option"));
}
});
@ -671,18 +671,22 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase:
export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: boolean = true, destroy: boolean = true, duration: number = 750): Promise<boolean> {
return new Promise(resolve => {
const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals;
const enemyPokemon = scene.getEnemyField();
if (enemyPokemon) {
scene.currentBattle.enemyParty = [];
}
if (introVisuals) {
if (!hide) {
// Make sure visuals are in proper state for showing
introVisuals.setVisible(true);
introVisuals.x += 16;
introVisuals.y -= 16;
introVisuals.x = 244;
introVisuals.y = 60;
introVisuals.alpha = 0;
}
// Transition
scene.tweens.add({
targets: introVisuals,
targets: [introVisuals, enemyPokemon],
x: `${hide? "+" : "-"}=16`,
y: `${hide ? "-" : "+"}=16`,
alpha: hide ? 0 : 1,
@ -690,9 +694,12 @@ export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide:
duration,
onComplete: () => {
if (hide && destroy) {
scene.field.remove(introVisuals);
introVisuals.setVisible(false);
introVisuals.destroy();
scene.field.remove(introVisuals, true);
enemyPokemon.forEach(pokemon => {
scene.field.remove(pokemon, true);
});
scene.currentBattle.mysteryEncounter.introVisuals = null;
}
resolve(true);

View File

@ -19,5 +19,7 @@ export enum MysteryEncounterType {
A_TRAINERS_TEST,
TRASH_TO_TREASURE,
BERRIES_ABOUND,
CLOWNING_AROUND
CLOWNING_AROUND,
PART_TIMER,
DANCING_LESSONS
}

View File

@ -19,19 +19,23 @@ import { aTrainersTestDialogue } from "#app/locales/en/mystery-encounters/a-trai
import { trashToTreasureDialogue } from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue";
import { berriesAboundDialogue } from "#app/locales/en/mystery-encounters/berries-abound-dialogue";
import { clowningAroundDialogue } from "#app/locales/en/mystery-encounters/clowning-around-dialogue";
import { partTimerDialogue } from "#app/locales/en/mystery-encounters/part-timer-dialogue";
import { dancingLessonsDialogue } from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue";
/**
* Patterns that can be used:
* '$' will be treated as a new line for Message and Dialogue strings
* '@d{<number>}' will add a time delay to text animation for Message and Dialogue strings
* Injection patterns that can be used:
* - `$` will be treated as a new line for Message and Dialogue strings.
* - `@d{<number>}` will add a time delay to text animation for Message and Dialogue strings.
* - `@s{<sound_effect_key>}` will play a specified sound effect for Message and Dialogue strings.
* - `@f{<number>}` will fade the screen to black for the given duration, then fade back in for Message and Dialogue strings.
* - `{{<token>}}` will auto-inject the matching dialogue token value that is stored in {@link IMysteryEncounter.dialogueTokens}.
* - (see [i18next interpolations](https://www.i18next.com/translation-function/interpolation)) for more details.
* - `@[<TextStyle>]{<text>}` will auto-color the given text to a specified {@link TextStyle} (e.g. `TextStyle.SUMMARY_GREEN`).
*
* '{{<token>}}' will auto-inject the matching token value for the specified Encounter that is stored in dialogueTokens
* (see [i18next interpolations](https://www.i18next.com/translation-function/interpolation))
*
* '@[<TextStyle>]{<text>}' will auto-color the given text to a specified TextStyle (e.g. TextStyle.SUMMARY_GREEN)
*
* Any '(+)' or '(-)' type of tooltip will auto-color to green/blue respectively. THIS ONLY OCCURS FOR OPTION TOOLTIPS, NOWHERE ELSE
* Other types of '(...)' tooltips will have to specify the text color manually by using '@[SUMMARY_GREEN]{<text>}' pattern
* For Option tooltips ({@link OptionTextDisplay.buttonTooltip}):
* - Any tooltip that starts with `(+)` or `(-)` at the beginning of a newline will auto-color to green/blue respectively.
* - Note, this only occurs for option tooltips, nowhere else.
* - Other types of `(...)` tooltips will have to specify the text color manually by using the `@[SUMMARY_GREEN]{<text>}` pattern.
*/
export const mysteryEncounter = {
// DO NOT REMOVE
@ -41,6 +45,7 @@ export const mysteryEncounter = {
"paid_money": "You paid ₽{{amount, number}}.",
"receive_money": "You received ₽{{amount, number}}!",
"affects_pokedex": "Affects Pokédex Data",
"cancel_option": "Return to encounter option select.",
mysteriousChallengers: mysteriousChallengersDialogue,
mysteriousChest: mysteriousChestDialogue,
@ -62,5 +67,7 @@ export const mysteryEncounter = {
aTrainersTest: aTrainersTestDialogue,
trashToTreasure: trashToTreasureDialogue,
berriesAbound: berriesAboundDialogue,
clowningAround: clowningAroundDialogue
clowningAround: clowningAroundDialogue,
partTimer: partTimerDialogue,
dancingLessons: dancingLessonsDialogue
} as const;

View File

@ -1,5 +1,5 @@
export const absoluteAvariceDialogue = {
intro: "A Greedent ambushed you\nand stole your party's berries!",
intro: "A Greedent ambushes you\nand steals your party's berries!",
title: "Absolute Avarice",
description: "The Greedent has caught you totally off guard now all your berries are gone!\n\nThe Greedent looks like it's about to eat them when it pauses to look at you, interested.",
query: "What will you do?",

View File

@ -0,0 +1,30 @@
export const dancingLessonsDialogue = {
intro: "An Oricorio dances sadly alone, without a partner.",
title: "Dancing Lessons",
description: "The Oricorio doesn't seem aggressive, if anything it seems sad.\n\nMaybe it just wants someone to dance with...",
query: "What will you do?",
option: {
1: {
label: "Battle It",
tooltip: "(-) Tough Battle\n(+) Gain a Baton",
selected: "The Oricorio is distraught and moves to defend itself!",
boss_enraged: "The Oricorio's fear boosted its stats!"
},
2: {
label: "Learn Its Dance",
tooltip: "(+) Teach a Pokémon Revelation Dance",
selected: `You watch the Oricorio closely as it performs its dance...
$@s{level_up_fanfare}Your {{selectedPokemon}} wants to learn Revelation Dance!`,
},
3: {
label: "Show It a Dance",
tooltip: "(-) Teach the Oricorio a Dance Move\n(+) The Oricorio Will Like You",
disabled_tooltip: "Your Pokémon need to know a Dance move for this.",
select_prompt: "Select a Dance type move to use.",
selected: `The Oricorio watches in fascination as\n{{selectedPokemon}} shows off {{selectedMove}}!
$It loves the display!
$@s{level_up_fanfare}The Oricorio wants to join your party!`,
},
},
invalid_selection: "This Pokémon doesn't know a Dance move"
};

View File

@ -0,0 +1,34 @@
export const partTimerDialogue = {
intro: "A busy worker flags you down.",
speaker: "Worker",
intro_dialogue: `You look like someone with lots of capable Pokémon!
$We can pay you if you're able to help us with some part-time work!`,
title: "Part-Timer",
description: "Looks like there are plenty of tasks that need to be done. Depending how well-suited your Pokémon is to a task, they might earn more or less money.",
query: "Which job will you choose?",
invalid_selection: "Pokémon must be healthy enough.",
option: {
1: {
label: "Make Deliveries",
tooltip: "(-) Your Pokémon Uses its Speed\n(+) Earn @[MONEY]{Money}",
selected: "Your {{selectedPokemon}} works a shift delivering orders to customers.",
},
2: {
label: "Warehouse Work",
tooltip: "(-) Your Pokémon Uses its Strength and Endurance\n(+) Earn @[MONEY]{Money}",
selected: "Your {{selectedPokemon}} works a shift moving items around the warehouse.",
},
3: {
label: "Sales Assistant",
tooltip: "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}",
disabled_tooltip: "Your Pokémon need to know certain moves for this job",
selected: "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to attract customers to the business!",
},
},
job_complete_good: `Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!
$Here's your check for the day.`,
job_complete_bad: `Your {{selectedPokemon}} helped us out a bit!
$Here's your check for the day.`,
pokemon_tired: "Your {{selectedPokemon}} is worn out!\nThe PP of all its moves was reduced to 2!",
outro: "Come back and help out again sometime!"
};

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.DANCING_LESSONS;
// -------------------------
// MODIFIER / ITEM OVERRIDES

View File

@ -1,11 +1,12 @@
import BattleScene, { bypassLogin } from "./battle-scene";
import { DamageResult, default as Pokemon, EnemyPokemon, FieldPosition, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "./field/pokemon";
import * as Utils from "./utils";
import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move";
import { isNullOrUndefined } from "./utils";
import { allMoves, applyFilteredMoveAttrs, applyMoveAttrs, AttackMove, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, FixedDamageAttr, ForceSwitchOutAttr, getMoveTargets, HealStatusEffectAttr, HitsTagAttr, IncrementMovePriorityAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveEffectTrigger, MoveFlags, MoveTarget, MoveTargetSet, MultiHitAttr, NoEffectAttr, OverrideMoveEffectAttr, PostVictoryStatChangeAttr, PreMoveMessageAttr, SelfStatusMove, VariableTargetAttr } from "./data/move";
import { Mode } from "./ui/ui";
import { Command } from "./ui/command-ui-handler";
import { Stat } from "./data/pokemon-stat";
import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, PokemonResetNegativeStatStageModifier } from "./modifier/modifier";
import { BerryModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, IvScannerModifier, LapsingPersistentModifier, LapsingPokemonHeldItemModifier, MapModifier, Modifier, MoneyInterestModifier, MoneyMultiplierModifier, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PokemonMultiHitModifier, PokemonResetNegativeStatStageModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier } from "./modifier/modifier";
import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball";
import { CommonAnim, CommonBattleAnim, initEncounterAnims, initMoveAnim, loadEncounterAnimAssets, loadMoveAnimAssets, MoveAnim } from "./data/battle-anims";
@ -23,10 +24,12 @@ import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, MysteryEncounterP
import { getPokemonMessage, getPokemonNameWithAffix } from "./messages";
import { Starter } from "./ui/starter-select-ui-handler";
import { Gender } from "./data/gender";
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
import { getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage, Weather, WeatherType } from "./data/weather";
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability";
import { Unlockables, getUnlockableName } from "./system/unlockables";
import {
AddSecondStrikeAbAttr, AlwaysHitAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostFaintAbAttrs, applyPostKnockOutAbAttrs, applyPostMoveUsedAbAttrs, applyPostStatChangeAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostVictoryAbAttrs, applyPostWeatherLapseAbAttrs, applyPreAttackAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, BlockRedirectAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, CheckTrappedAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, IncreasePpAbAttr, IncrementMovePriorityAbAttr, MaxMultiHitAbAttr, PokemonTypeChangeAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostBiomeChangeAbAttr, PostDefendAbAttr, PostFaintAbAttr, PostKnockOutAbAttr, PostMoveUsedAbAttr, PostStatChangeAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostVictoryAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreventBerryUseAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, RunSuccessAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr
} from "./data/ability";
import { getUnlockableName, Unlockables } from "./system/unlockables";
import { getBiomeKey } from "./field/arena";
import { BattlerIndex, BattleType, TurnCommand } from "./battle";
import { achvs, ChallengeAchv, HealAchv, LevelAchv } from "./system/achv";
@ -48,7 +51,7 @@ import { fetchDailyRunSeed, getDailyRunStarters } from "./data/daily-run";
import { GameMode, GameModes, getGameMode } from "./game-mode";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species";
import i18next from "./plugins/i18n";
import { TextStyle, addTextObject } from "./ui/text";
import { addTextObject, TextStyle } from "./ui/text";
import { Type } from "./data/type";
import { BerryUsedEvent, EncounterPhaseEvent, MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./events/battle-scene";
import { Abilities } from "#enums/abilities";
@ -69,7 +72,6 @@ import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifie
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import Overrides from "#app/overrides";
import { isNullOrUndefined } from "./utils";
const { t } = i18next;
@ -859,7 +861,7 @@ export class EncounterPhase extends BattlePhase {
if (!this.loaded) {
if (battle.battleType === BattleType.TRAINER) {
battle.enemyParty[e] = battle.trainer.genPartyMember(e);
} else {
} else if (battle.battleType !== BattleType.MYSTERY_ENCOUNTER) {
const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true);
battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies));
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
@ -949,8 +951,7 @@ export class EncounterPhase extends BattlePhase {
enemyPokemon.setVisible(false);
this.scene.currentBattle.trainer.tint(0, 0.5);
} else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) {
enemyPokemon.setVisible(false);
this.scene.currentBattle?.trainer?.tint(0, 0.5);
// TODO: this may not be necessary, but leaving as placeholder
}
if (battle.double) {
enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT);

View File

@ -0,0 +1,254 @@
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 { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils";
import { SelectModifierPhase } from "#app/phases";
import BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter";
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.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
]);
CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => {
biomeMap.set(biome, [MysteryEncounterType.PART_TIMER]);
});
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.PART_TIMER, defaultParty);
expect(PartTimerEncounter.encounterType).toBe(MysteryEncounterType.PART_TIMER);
expect(PartTimerEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON);
expect(PartTimerEncounter.dialogue).toBeDefined();
expect(PartTimerEncounter.dialogue.intro).toStrictEqual([
{ text: `${namespace}.intro` },
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`,
}
]);
expect(PartTimerEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`);
expect(PartTimerEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`);
expect(PartTimerEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`);
expect(PartTimerEncounter.options.length).toBe(3);
});
it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - Make Deliveries", () => {
it("should have the correct properties", () => {
const option = PartTimerEncounter.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: [
{
text: `${namespace}.option.1.selected`
}
]
});
});
it("should have shop with only TMs", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(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.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 1);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 2 - Vitamin Shop", () => {
it("should have the correct properties", () => {
const option = PartTimerEncounter.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 have shop with only Vitamins", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(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.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - Assist with Sales", () => {
it("should have the correct properties", () => {
const option = PartTimerEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
selected: [
{
text: `${namespace}.option.3.selected`
}
]
});
});
it("should have shop with only X Items", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(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.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 4 - Pokeball Shop", () => {
it("should have the correct properties", () => {
const option = PartTimerEncounter.options[3];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.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.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(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.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 4);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});