Merge pull request #139 from AsdarDevelops/part-timer

Part-Timer and Dancing Lessons Encounters
This commit is contained in:
ImperialSympathizer 2024-08-12 10:40:56 -04:00 committed by GitHub
commit 741137d74b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2299 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,294 @@
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, 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";
import { modifierTypes } from "#app/modifier/modifier-type";
/** 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
});
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.BATON], fillRemaining: 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.disabled_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());
encounter.misc.selectedMove = move;
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 encounter = scene.currentBattle.mysteryEncounter;
const oricorio = encounter.misc.oricorioData.toPokemon(scene);
oricorio.passive = true;
// Ensure the Oricorio's moveset gains the Dance move the player used
const move = encounter.misc.selectedMove?.getMove().id;
if (!oricorio.moveset.some(m => m.getMove().id === move)) {
if (oricorio.moveset.length < 4) {
oricorio.moveset.push(new PokemonMove(move));
} else {
oricorio.moveset[3] = new PokemonMove(move);
}
}
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true);
})
.build()
)
.build();

View File

@ -0,0 +1,333 @@
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, 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 (90 base stat, 16 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 * 90 + 16) * 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;
});
setEncounterExp(scene, pokemon.id, 100);
// 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 (75 base stat, 16 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 * 75 + 16) * pokemon.level) * 0.01) + pokemon.level + 10;
const baselineAtkDef = Math.floor(((2 * 75 + 16) * 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;
});
setEncounterExp(scene, pokemon.id, 100);
// 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;
});
setEncounterExp(scene, selectedPokemon.id, 100);
// 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

@ -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;
@ -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

@ -18,7 +18,7 @@ import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler";
* @param secondaryOptionSelect -
* @param isBattle - if selecting option should lead to battle, set to true
*/
export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo: number } = null, isBattle: boolean = false) {
export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo?: number } = null, isBattle: boolean = false) {
vi.spyOn(EncounterPhaseUtils, "selectPokemonForOption");
await runSelectMysteryEncounterOption(game, optionNo, secondaryOptionSelect);
@ -65,7 +65,7 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
}
}
export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo: number } = null) {
export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo?: number } = null) {
// Handle any eventual queued messages (e.g. weather phase, etc.)
game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
@ -112,7 +112,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN
}
}
async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo: number) {
async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo?: number) {
// Handle secondary option selections
const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler;
vi.spyOn(partyUiHandler, "show");

View File

@ -0,0 +1,246 @@
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, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { CommandPhase, LearnMovePhase, MovePhase, SelectModifierPhase } from "#app/phases";
import { Moves } from "#enums/moves";
import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
const namespace = "mysteryEncounter:dancingLessons";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.PLAINS;
const defaultWave = 45;
describe("Dancing Lessons - 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);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
[Biome.PLAINS, [MysteryEncounterType.DANCING_LESSONS]],
[Biome.SPACE, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
expect(DancingLessonsEncounter.encounterType).toBe(MysteryEncounterType.DANCING_LESSONS);
expect(DancingLessonsEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT);
expect(DancingLessonsEncounter.dialogue).toBeDefined();
expect(DancingLessonsEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]);
expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`);
expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`);
expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`);
expect(DancingLessonsEncounter.options.length).toBe(3);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of proper biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.SPACE);
await game.runToMysteryEncounter();
expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS);
});
describe("Option 1 - Fight the Oricorio", () => {
it("should have the correct properties", () => {
const option1 = DancingLessonsEncounter.options[0];
expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.1.selected`,
},
],
});
});
it("should start battle against Oricorio", async () => {
const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(Species.ORICORIO);
expect(enemyField[0].summonData.battleStats).toEqual([1, 1, 1, 1, 1, 0, 0]);
const moveset = enemyField[0].moveset.map(m => m.moveId);
expect(moveset.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy();
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(1);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.REVELATION_DANCE).length).toBe(1); // Revelation Dance used before battle
});
it("should have a Baton in the rewards after battle", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
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); // Should fill remaining
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("BATON");
});
});
describe("Option 2 - Learn its Dance", () => {
it("should have the correct properties", () => {
const option = DancingLessonsEncounter.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 select a pokemon to learn Revelation Dance", async () => {
const phaseSpy = vi.spyOn(scene, "unshiftPhase");
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 });
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof LearnMovePhase).map(p => p[0]);
expect(movePhases.length).toBe(1);
expect(movePhases.filter(p => (p as LearnMovePhase)["moveId"] === Moves.REVELATION_DANCE).length).toBe(1); // Revelation Dance taught to pokemon
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 });
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - Teach it a Dance", () => {
it("should have the correct properties", () => {
const option = DancingLessonsEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`,
secondOptionPrompt: `${namespace}.option.3.select_prompt`,
selected: [
{
text: `${namespace}.option.3.selected`,
},
],
});
});
it("should add Oricorio to the party", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
const partyCountBefore = scene.getParty().length;
scene.getParty()[0].moveset = [new PokemonMove(Moves.DRAGON_DANCE)];
await runMysteryEncounterToEnd(game, 3, {pokemonNo: 1, optionNo: 1});
const partyCountAfter = scene.getParty().length;
expect(partyCountBefore + 1).toBe(partyCountAfter);
const oricorio = scene.getParty()[scene.getParty().length - 1];
expect(oricorio.species.speciesId).toBe(Species.ORICORIO);
const moveset = oricorio.moveset.map(m => m.moveId);
expect(moveset?.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy();
expect(moveset?.some(m => m === Moves.DRAGON_DANCE)).toBeTruthy();
});
it("should NOT be selectable if the player doesn't have a Dance type move", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
const partyCountBefore = scene.getParty().length;
scene.getParty().forEach(p => p.moveset = []);
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, 3);
const partyCountAfter = scene.getParty().length;
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();
expect(partyCountBefore).toBe(partyCountAfter);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty);
scene.getParty()[0].moveset = [new PokemonMove(Moves.DRAGON_DANCE)];
await runMysteryEncounterToEnd(game, 3, {pokemonNo: 1, optionNo: 1});
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});

View File

@ -0,0 +1,295 @@
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, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils";
import BattleScene from "#app/battle-scene";
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";
import { PokemonMove } from "#app/field/pokemon";
import { Moves } from "#enums/moves";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
const namespace = "mysteryEncounter:partTimer";
// Pyukumuku for lowest speed, Regieleki for highest speed, Feebas for lowest "bulk", Melmetal for highest "bulk"
const defaultParty = [Species.PYUKUMUKU, Species.REGIELEKI, Species.FEEBAS, Species.MELMETAL];
const defaultBiome = Biome.PLAINS;
const defaultWave = 37;
describe("Part-Timer - 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 give the player 1x money multiplier money with max slowest Pokemon", async () => {
vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
// Override party levels to 50 so stats can be fully reflective
scene.getParty().forEach(p => {
p.level = 50;
p.calculateStats();
});
await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 });
expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false);
// Expect PP of mon's moves to have been reduced to 2
const moves = scene.getParty()[0].moveset;
for (const move of moves) {
expect(move.getMovePp() - move.ppUsed).toBe(2);
}
});
it("should give the player 4x money multiplier money with max fastest Pokemon", async () => {
vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
// Override party levels to 50 so stats can be fully reflective
scene.getParty().forEach(p => {
p.level = 50;
p.ivs = [20,20,20,20,20,20];
p.calculateStats();
});
await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2 });
expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false);
// Expect PP of mon's moves to have been reduced to 2
const moves = scene.getParty()[1].moveset;
for (const move of moves) {
expect(move.getMovePp() - move.ppUsed).toBe(2);
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 });
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 2 - Help in the Warehouse", () => {
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 give the player 1x money multiplier money with least bulky Pokemon", async () => {
vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
// Override party levels to 50 so stats can be fully reflective
scene.getParty().forEach(p => {
p.level = 50;
p.calculateStats();
});
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 });
expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false);
// Expect PP of mon's moves to have been reduced to 2
const moves = scene.getParty()[2].moveset;
for (const move of moves) {
expect(move.getMovePp() - move.ppUsed).toBe(2);
}
});
it("should give the player 4x money multiplier money with bulkiest Pokemon", async () => {
vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
// Override party levels to 50 so stats can be fully reflective
scene.getParty().forEach(p => {
p.level = 50;
p.ivs = [20,20,20,20,20,20];
p.calculateStats();
});
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 4 });
expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false);
// Expect PP of mon's moves to have been reduced to 2
const moves = scene.getParty()[3].moveset;
for (const move of moves) {
expect(move.getMovePp() - move.ppUsed).toBe(2);
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 });
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.DISABLED_OR_SPECIAL);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.3.selected`
}
]
});
});
it("Should NOT be selectable when requirements are not met", async () => {
vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
// Mock movesets
scene.getParty().forEach(p => p.moveset = []);
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, 3);
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();
expect(EncounterPhaseUtils.updatePlayerMoney).not.toHaveBeenCalled();
});
it("should be selectable and give the player 2.5x money multiplier money with requirements met", async () => {
vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
// Mock moveset
scene.getParty()[0].moveset = [new PokemonMove(Moves.ATTRACT)];
await runMysteryEncounterToEnd(game, 3);
expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(2.5), true, false);
// Expect PP of mon's moves to have been reduced to 2
const moves = scene.getParty()[0].moveset;
for (const move of moves) {
expect(move.getMovePp() - move.ppUsed).toBe(2);
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 });
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});