Merge pull request #118 from AsdarDevelops/absolute-avarice
[Event] Absolute Avarice
This commit is contained in:
commit
8b10756644
|
@ -1072,12 +1072,14 @@ export default class BattleScene extends SceneBase {
|
|||
this.field.add(newTrainer);
|
||||
}
|
||||
|
||||
// TODO: remove this once spawn rates are finalized
|
||||
// TODO: remove these once ME spawn rates are finalized
|
||||
// let testStartingWeight = 0;
|
||||
// while (testStartingWeight < 3) {
|
||||
// calculateMEAggregateStats(this, testStartingWeight);
|
||||
// testStartingWeight += 2;
|
||||
// }
|
||||
// calculateRareSpawnAggregateStats(this, 14);
|
||||
|
||||
// Check for mystery encounter
|
||||
// Can only occur in place of a standard wild battle, waves 10-180
|
||||
if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && newWaveIndex < 180 && newWaveIndex > 10) {
|
||||
|
@ -2684,18 +2686,22 @@ export default class BattleScene extends SceneBase {
|
|||
while (availableEncounters.length === 0 && tier >= 0) {
|
||||
availableEncounters = biomeMysteryEncounters
|
||||
.filter((encounterType) => {
|
||||
if (allMysteryEncounters[encounterType].encounterTier !== tier) { // Encounter is in tier
|
||||
const encounterCandidate = allMysteryEncounters[encounterType];
|
||||
if (!encounterCandidate) {
|
||||
return false;
|
||||
}
|
||||
if (!allMysteryEncounters[encounterType]?.meetsRequirements(this)) { // Meets encounter requirements
|
||||
if (encounterCandidate.encounterTier !== tier) { // Encounter is in tier
|
||||
return false;
|
||||
}
|
||||
if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements
|
||||
return false;
|
||||
}
|
||||
if (!isNullOrUndefined(previousEncounter) && encounterType === previousEncounter) { // Previous encounter was not this one
|
||||
return false;
|
||||
}
|
||||
if (this.mysteryEncounterData.encounteredEvents?.length > 0 && // Encounter has not exceeded max allowed encounters
|
||||
allMysteryEncounters[encounterType].maxAllowedEncounters > 0
|
||||
&& this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === encounterType).length >= allMysteryEncounters[encounterType].maxAllowedEncounters) {
|
||||
encounterCandidate.maxAllowedEncounters > 0
|
||||
&& this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === encounterType).length >= encounterCandidate.maxAllowedEncounters) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,504 @@
|
|||
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import Pokemon, { PokemonMove } from "#app/field/pokemon";
|
||||
import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
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 { PersistentModifierRequirement } from "../mystery-encounter-requirements";
|
||||
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { BerryModifier } from "#app/modifier/modifier";
|
||||
import { StatChangePhase } from "#app/phases";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BattleStat } from "#app/data/battle-stat";
|
||||
import { randInt } from "#app/utils";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { TrainerSlot } from "#app/data/trainer-config";
|
||||
import { PokeballType } from "#app/data/pokeball";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounter:absoluteAvarice";
|
||||
|
||||
/**
|
||||
* Absolute Avarice encounter.
|
||||
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/58 | GitHub Issue #58}
|
||||
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
|
||||
*/
|
||||
export const AbsoluteAvariceEncounter: IMysteryEncounter =
|
||||
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.ABSOLUTE_AVARICE)
|
||||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withSceneWaveRangeRequirement(10, 180)
|
||||
.withSceneRequirement(new PersistentModifierRequirement(BerryModifier.name, 4)) // Must have at least 4 berries to spawn
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: Species.GREEDENT.toString(),
|
||||
fileRoot: "pokemon",
|
||||
hasShadow: false,
|
||||
repeat: true,
|
||||
x: -5
|
||||
},
|
||||
{
|
||||
// This sprite has the shadow
|
||||
spriteKey: Species.GREEDENT.toString(),
|
||||
fileRoot: "pokemon",
|
||||
hasShadow: true,
|
||||
alpha: 0.001,
|
||||
repeat: true,
|
||||
x: -5
|
||||
},
|
||||
{
|
||||
spriteKey: "lum_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 7,
|
||||
y: -14,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "salac_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 2,
|
||||
y: 4,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "lansat_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 32,
|
||||
y: 5,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "liechi_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 6,
|
||||
y: -5,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "sitrus_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 7,
|
||||
y: 8,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "enigma_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 26,
|
||||
y: -4,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "leppa_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 16,
|
||||
y: -27,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "petaya_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 30,
|
||||
y: -17,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "ganlon_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 16,
|
||||
y: -11,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "apicot_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 14,
|
||||
y: -2,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
{
|
||||
spriteKey: "starf_berry",
|
||||
fileRoot: "items",
|
||||
isItem: true,
|
||||
x: 18,
|
||||
y: 9,
|
||||
hidden: true,
|
||||
disableAnimation: true
|
||||
},
|
||||
])
|
||||
.withHideWildIntroMessage(true)
|
||||
.withAutoHideIntroVisuals(false)
|
||||
.withOnVisualsStart((scene: BattleScene) => {
|
||||
doGreedentSpriteSteal(scene);
|
||||
doBerrySpritePile(scene);
|
||||
|
||||
return true;
|
||||
})
|
||||
.withIntroDialogue([
|
||||
{
|
||||
text: `${namespace}:intro`,
|
||||
}
|
||||
])
|
||||
.withTitle(`${namespace}:title`)
|
||||
.withDescription(`${namespace}:description`)
|
||||
.withQuery(`${namespace}:query`)
|
||||
.withOnInit((scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
|
||||
scene.loadSe("PRSFX- Bug Bite", "battle_anims");
|
||||
scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3");
|
||||
|
||||
// Get all player berry items, remove from party, and store reference
|
||||
const berryItems = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[];
|
||||
|
||||
// Sort berries by party member ID to more easily re-add later if necessary
|
||||
const berryItemsMap = new Map<number, BerryModifier[]>();
|
||||
scene.getParty().forEach(pokemon => {
|
||||
const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id);
|
||||
if (pokemonBerries?.length > 0) {
|
||||
berryItemsMap.set(pokemon.id, pokemonBerries);
|
||||
}
|
||||
});
|
||||
|
||||
encounter.misc = { berryItemsMap };
|
||||
|
||||
// Generates copies of the stolen berries to put on the Greedent
|
||||
const bossModifierTypes: PokemonHeldItemModifierType[] = [];
|
||||
berryItems.forEach(berryMod => {
|
||||
// Can't define stack count on a ModifierType, have to just create separate instances for each stack
|
||||
// Overflow berries will be "lost" on the boss, but it's un-catchable anyway
|
||||
for (let i = 0; i < berryMod.stackCount; i++) {
|
||||
const modifierType = generateModifierTypeOption(scene, modifierTypes.BERRY, [berryMod.berryType]).type as PokemonHeldItemModifierType;
|
||||
bossModifierTypes.push(modifierType);
|
||||
}
|
||||
|
||||
scene.removeModifier(berryMod);
|
||||
});
|
||||
|
||||
// Calculate boss mon
|
||||
const config: EnemyPartyConfig = {
|
||||
levelAdditiveMultiplier: 1,
|
||||
pokemonConfigs: [
|
||||
{
|
||||
species: getPokemonSpecies(Species.GREEDENT),
|
||||
isBoss: true,
|
||||
bossSegments: 3,
|
||||
moveSet: [Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF],
|
||||
modifierTypes: bossModifierTypes,
|
||||
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];
|
||||
|
||||
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;
|
||||
|
||||
// Provides 1x Reviver Seed to each party member at end of battle
|
||||
const revSeed = generateModifierTypeOption(scene, modifierTypes.REVIVER_SEED).type;
|
||||
const givePartyPokemonReviverSeeds = () => {
|
||||
const party = scene.getParty();
|
||||
party.forEach(p => {
|
||||
const seedModifier = revSeed.newModifier(p);
|
||||
scene.addModifier(seedModifier, false, false, false, true);
|
||||
});
|
||||
queueEncounterMessage(scene, `${namespace}:option:1:food_stash`);
|
||||
};
|
||||
|
||||
setEncounterRewards(scene, { fillRemaining: true }, null, givePartyPokemonReviverSeeds);
|
||||
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]);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.withOption(
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}:option:2:label`,
|
||||
buttonTooltip: `${namespace}:option:2:tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}:option:2:selected`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const berryMap = encounter.misc.berryItemsMap;
|
||||
|
||||
// Returns 2/5 of the berries stolen from each Pokemon
|
||||
const party = scene.getParty();
|
||||
party.forEach(pokemon => {
|
||||
const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id);
|
||||
const berryTypesAsArray = [];
|
||||
stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType)));
|
||||
const returnedBerryCount = Math.floor((berryTypesAsArray.length ?? 0) * 2 / 5);
|
||||
|
||||
if (returnedBerryCount > 0) {
|
||||
for (let i = 0; i < returnedBerryCount; i++) {
|
||||
// Shuffle remaining berry types and pop
|
||||
Phaser.Math.RND.shuffle(berryTypesAsArray);
|
||||
const randBerryType = berryTypesAsArray.pop();
|
||||
|
||||
const berryModType = generateModifierTypeOption(scene, modifierTypes.BERRY, [randBerryType]).type as BerryModifierType;
|
||||
applyModifierTypeToPlayerPokemon(scene, pokemon, berryModType);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.withOption(
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}:option:3:label`,
|
||||
buttonTooltip: `${namespace}:option:3:tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}:option:3:selected`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.withPreOptionPhase(async (scene: BattleScene) => {
|
||||
// Animate berries being eaten
|
||||
doGreedentEatBerries(scene);
|
||||
doBerrySpritePile(scene, true);
|
||||
return true;
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Let it have the food
|
||||
// Greedent joins the team, level equal to 2 below highest party member
|
||||
const level = getHighestLevelPlayerPokemon(scene).level - 2;
|
||||
const greedent = scene.addEnemyPokemon(getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false);
|
||||
greedent.moveset = [new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF)];
|
||||
greedent.passive = true;
|
||||
|
||||
await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false);
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
function doGreedentSpriteSteal(scene: BattleScene) {
|
||||
const shakeDelay = 50;
|
||||
const slideDelay = 500;
|
||||
|
||||
const greedentSprites = scene.currentBattle.mysteryEncounter.introVisuals.getSpriteAtIndex(0);
|
||||
|
||||
scene.playSound("Follow Me");
|
||||
scene.tweens.chain({
|
||||
targets: greedentSprites,
|
||||
tweens: [
|
||||
{ // Slide Greedent diagonally
|
||||
duration: slideDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
y: "+=75",
|
||||
x: "-=65",
|
||||
scale: 1.1
|
||||
},
|
||||
{ // Shake
|
||||
duration: shakeDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
x: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
y: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
},
|
||||
{ // Shake
|
||||
duration: shakeDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
x: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
y: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
},
|
||||
{ // Shake
|
||||
duration: shakeDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
x: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
y: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
},
|
||||
{ // Shake
|
||||
duration: shakeDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
x: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
y: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
},
|
||||
{ // Shake
|
||||
duration: shakeDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
x: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
y: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
},
|
||||
{ // Shake
|
||||
duration: shakeDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
x: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
y: (randInt(2) > 0 ? "-=" : "+=") + 5,
|
||||
},
|
||||
{ // Slide Greedent diagonally
|
||||
duration: slideDelay,
|
||||
ease: "Cubic.easeOut",
|
||||
y: "-=75",
|
||||
x: "+=65",
|
||||
scale: 1
|
||||
},
|
||||
{ // Bounce at the end
|
||||
duration: 300,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
y: "-=20",
|
||||
loop: 1,
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function doGreedentEatBerries(scene: BattleScene) {
|
||||
const greedentSprites = scene.currentBattle.mysteryEncounter.introVisuals.getSpriteAtIndex(0);
|
||||
let index = 1;
|
||||
scene.tweens.add({
|
||||
targets: greedentSprites,
|
||||
duration: 150,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
y: "-=8",
|
||||
loop: 5,
|
||||
onStart: () => {
|
||||
scene.playSound("PRSFX- Bug Bite");
|
||||
},
|
||||
onLoop: () => {
|
||||
if (index % 2 === 0) {
|
||||
scene.playSound("PRSFX- Bug Bite");
|
||||
}
|
||||
index++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scene
|
||||
* @param isEat - default false. Will "create" pile when false, and remove pile when true.
|
||||
*/
|
||||
function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) {
|
||||
const berryAddDelay = 150;
|
||||
let animationOrder = ["starf", "sitrus", "lansat", "salac", "apicot", "enigma", "liechi", "ganlon", "lum", "petaya", "leppa"];
|
||||
if (isEat) {
|
||||
animationOrder = animationOrder.reverse();
|
||||
}
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
animationOrder.forEach((berry, i) => {
|
||||
const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey.includes(berry));
|
||||
const [ sprite, tintSprite ] = encounter.introVisuals.getSpriteAtIndex(introVisualsIndex);
|
||||
scene.time.delayedCall(berryAddDelay * i + 400, () => {
|
||||
if (sprite) {
|
||||
sprite.setVisible(!isEat);
|
||||
}
|
||||
if (tintSprite) {
|
||||
tintSprite.setVisible(!isEat);
|
||||
}
|
||||
|
||||
// Animate Petaya berry falling off the pile
|
||||
if (berry === "petaya" && sprite && tintSprite && !isEat) {
|
||||
scene.time.delayedCall(200, () => {
|
||||
doBerryBounce(scene, [sprite, tintSprite], 30, 500);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doBerryBounce(scene: BattleScene, berrySprites: Phaser.GameObjects.Sprite[], yd: number, baseBounceDuration: integer) {
|
||||
let bouncePower = 1;
|
||||
let bounceYOffset = yd;
|
||||
|
||||
const doBounce = () => {
|
||||
scene.tweens.add({
|
||||
targets: berrySprites,
|
||||
y: "+=" + bounceYOffset,
|
||||
x: { value: "+=" + (bouncePower * bouncePower * 10), ease: "Linear" },
|
||||
duration: bouncePower * baseBounceDuration,
|
||||
ease: "Cubic.easeIn",
|
||||
onComplete: () => {
|
||||
bouncePower = bouncePower > 0.01 ? bouncePower * 0.5 : 0;
|
||||
|
||||
if (bouncePower) {
|
||||
bounceYOffset = bounceYOffset * bouncePower;
|
||||
|
||||
scene.tweens.add({
|
||||
targets: berrySprites,
|
||||
y: "-=" + bounceYOffset,
|
||||
x: { value: "+=" + (bouncePower * bouncePower * 10), ease: "Linear" },
|
||||
duration: bouncePower * baseBounceDuration,
|
||||
ease: "Cubic.easeOut",
|
||||
onComplete: () => doBounce()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
doBounce();
|
||||
}
|
|
@ -1,18 +1,20 @@
|
|||
import { leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
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 { HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements";
|
||||
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements";
|
||||
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { BerryModifier, PokemonBaseStatModifier, PokemonBaseStatTotalModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, TerastallizeModifier } from "#app/modifier/modifier";
|
||||
import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonBaseStatModifier, PokemonBaseStatTotalModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PreserveBerryModifier, TerastallizeModifier } from "#app/modifier/modifier";
|
||||
import { ModifierRewardPhase } from "#app/phases";
|
||||
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
|
||||
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounter:delibirdy";
|
||||
|
@ -39,6 +41,10 @@ export const DelibirdyEncounter: IMysteryEncounter =
|
|||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withSceneWaveRangeRequirement(10, 180)
|
||||
.withSceneRequirement(new MoneyRequirement(0, 2.75)) // Must have enough money for it to spawn at the very least
|
||||
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn
|
||||
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
|
||||
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)
|
||||
))
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: Species.DELIBIRD.toString(),
|
||||
|
@ -82,7 +88,7 @@ export const DelibirdyEncounter: IMysteryEncounter =
|
|||
.withOption(
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||
.withSceneMoneyRequirement(0, 2.75)
|
||||
.withSceneMoneyRequirement(0, 2.75) // Must have money to spawn
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}:option:1:label`,
|
||||
buttonTooltip: `${namespace}:option:1:tooltip`,
|
||||
|
@ -99,7 +105,19 @@ export const DelibirdyEncounter: IMysteryEncounter =
|
|||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Give the player an Ability Charm
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM));
|
||||
// Check if the player has max stacks of that item already
|
||||
const existing = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier;
|
||||
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
// At max stacks, give the first party pokemon a Shell Bell instead
|
||||
const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType;
|
||||
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell);
|
||||
scene.playSound("item_fanfare");
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true);
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM));
|
||||
}
|
||||
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
.build()
|
||||
|
@ -159,11 +177,34 @@ export const DelibirdyEncounter: IMysteryEncounter =
|
|||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const modifier = encounter.misc.chosenModifier;
|
||||
|
||||
// Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed
|
||||
if (modifier.type.name.includes("Berry")) {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR));
|
||||
// Check if the player has max stacks of that Candy Jar already
|
||||
const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier;
|
||||
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
// At max stacks, give the first party pokemon a Shell Bell instead
|
||||
const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType;
|
||||
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell);
|
||||
scene.playSound("item_fanfare");
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true);
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR));
|
||||
}
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
|
||||
// Check if the player has max stacks of that Healing Charm already
|
||||
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
|
||||
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
// At max stacks, give the first party pokemon a Shell Bell instead
|
||||
const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType;
|
||||
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell);
|
||||
scene.playSound("item_fanfare");
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true);
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the modifier if its stacks go to 0
|
||||
|
@ -231,8 +272,19 @@ export const DelibirdyEncounter: IMysteryEncounter =
|
|||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const modifier = encounter.misc.chosenModifier;
|
||||
// Give the player a Berry Pouch
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
|
||||
|
||||
// Check if the player has max stacks of Berry Pouch already
|
||||
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
|
||||
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
// At max stacks, give the first party pokemon a Shell Bell instead
|
||||
const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType;
|
||||
await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell);
|
||||
scene.playSound("item_fanfare");
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true);
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
|
||||
}
|
||||
|
||||
// Remove the modifier if its stacks go to 0
|
||||
modifier.stackCount -= 1;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { modifierTypes, } from "#app/modifier/modifier-type";
|
||||
import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/modifier-type";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
|
||||
|
@ -17,7 +17,7 @@ import { WeatherType } from "#app/data/weather";
|
|||
import { isNullOrUndefined, randSeedInt } from "#app/utils";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
|
||||
|
@ -246,9 +246,8 @@ function giveLeadPokemonCharcoal(scene: BattleScene) {
|
|||
// Give first party pokemon Charcoal for free at end of battle
|
||||
const leadPokemon = scene.getParty()?.[0];
|
||||
if (leadPokemon) {
|
||||
const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]);
|
||||
scene.addModifier(charcoal.type.newModifier(leadPokemon), true);
|
||||
scene.updateModifiers();
|
||||
const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]).type as AttackTypeBoosterModifierType;
|
||||
applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal);
|
||||
scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name);
|
||||
queueEncounterMessage(scene, `${namespace}:found_charcoal`);
|
||||
}
|
||||
|
|
|
@ -160,7 +160,7 @@ export const FightOrFlightEncounter: IMysteryEncounter =
|
|||
config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
|
||||
config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
|
||||
pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(pokemon));
|
||||
queueEncounterMessage(pokemon.scene, `${namespace}:boss_enraged`);
|
||||
queueEncounterMessage(pokemon.scene, `${namespace}option:2:boss_enraged`);
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1));
|
||||
};
|
||||
await showEncounterText(scene, `${namespace}:option:2:bad_result`);
|
||||
|
|
|
@ -133,7 +133,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter =
|
|||
// "Catch" purchased pokemon
|
||||
const data = new PokemonData(purchasedPokemon);
|
||||
data.player = false;
|
||||
await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true);
|
||||
await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true, true);
|
||||
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter
|
|||
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
|
||||
import { MoneyRequirement } from "../mystery-encounter-requirements";
|
||||
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
|
||||
|
@ -110,10 +110,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
|
|||
const modifiers = encounter.misc.modifiers;
|
||||
|
||||
for (const modType of modifiers) {
|
||||
const modifier = modType.newModifier(chosenPokemon);
|
||||
await scene.addModifier(modifier, true, false, false, true);
|
||||
await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType);
|
||||
}
|
||||
scene.updateModifiers(true);
|
||||
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
})
|
||||
|
@ -195,10 +193,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
|
|||
const modifiers = encounter.misc.modifiers;
|
||||
|
||||
for (const modType of modifiers) {
|
||||
const modifier = modType.newModifier(chosenPokemon);
|
||||
await scene.addModifier(modifier, true, false, false, true);
|
||||
await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType);
|
||||
}
|
||||
scene.updateModifiers(true);
|
||||
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
})
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import { ModifierType } from "#app/modifier/modifier-type";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { isNullOrUndefined } from "#app/utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
|
@ -235,28 +234,36 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
|
|||
}
|
||||
|
||||
export class PersistentModifierRequirement extends EncounterSceneRequirement {
|
||||
requiredItems?: ModifierType[]; // TODO: not implemented
|
||||
constructor(item: ModifierType | ModifierType[]) {
|
||||
requiredHeldItemModifiers: string[];
|
||||
minNumberOfItems: number;
|
||||
|
||||
constructor(heldItem: string | string[], minNumberOfItems: number = 1) {
|
||||
super();
|
||||
this.requiredItems = Array.isArray(item) ? item : [item];
|
||||
this.minNumberOfItems = minNumberOfItems;
|
||||
this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem];
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const items = scene.modifiers;
|
||||
|
||||
if (!isNullOrUndefined(items) && this?.requiredItems.length > 0 && this.requiredItems.filter((searchingMod) =>
|
||||
items.filter((itemInScene) => itemInScene.type.id === searchingMod.id).length > 0).length === 0) {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
let modifierCount = 0;
|
||||
this.requiredHeldItemModifiers.forEach(modifier => {
|
||||
const matchingMods = scene.findModifiers(m => m.constructor.name === modifier);
|
||||
if (matchingMods?.length > 0) {
|
||||
matchingMods.forEach(matchingMod => {
|
||||
modifierCount += matchingMod.stackCount;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return modifierCount >= this.minNumberOfItems;
|
||||
}
|
||||
|
||||
getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
|
||||
const requiredItemsInInventory = this.requiredItems.filter((a) => {
|
||||
scene.modifiers.filter((itemInScene) => itemInScene.type.id === a.id).length > 0;
|
||||
});
|
||||
if (requiredItemsInInventory.length > 0) {
|
||||
return ["requiredItem", requiredItemsInInventory[0].name];
|
||||
if (this.requiredHeldItemModifiers.length > 0) {
|
||||
return ["requiredItem", this.requiredHeldItemModifiers[0]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ export default class IMysteryEncounter implements IMysteryEncounter {
|
|||
if (!isNullOrUndefined(encounter)) {
|
||||
Object.assign(this, encounter);
|
||||
}
|
||||
this.encounterTier = this.encounterTier ? this.encounterTier : MysteryEncounterTier.COMMON;
|
||||
this.encounterTier = !isNullOrUndefined(this.encounterTier) ? this.encounterTier : MysteryEncounterTier.COMMON;
|
||||
this.dialogue = this.dialogue ?? {};
|
||||
// Default max is 1 for ROGUE encounters, 3 for others
|
||||
this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3;
|
||||
|
@ -167,13 +167,13 @@ export default class IMysteryEncounter implements IMysteryEncounter {
|
|||
this.requirements = this.requirements ? this.requirements : [];
|
||||
this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false;
|
||||
this.autoHideIntroVisuals = !isNullOrUndefined(this.autoHideIntroVisuals) ? this.autoHideIntroVisuals : true;
|
||||
this.startOfBattleEffects = this.startOfBattleEffects ?? [];
|
||||
|
||||
// Reset any dirty flags or encounter data
|
||||
this.startOfBattleEffectsComplete = false;
|
||||
this.lockEncounterRewardTiers = true;
|
||||
this.dialogueTokens = {};
|
||||
this.enemyPartyConfigs = [];
|
||||
this.startOfBattleEffects = [];
|
||||
this.introVisuals = null;
|
||||
this.misc = null;
|
||||
this.expMultiplier = 1;
|
||||
|
|
|
@ -17,6 +17,7 @@ import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters
|
|||
import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-encounter";
|
||||
import { OfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter";
|
||||
import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter";
|
||||
import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-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;
|
||||
|
@ -238,7 +239,7 @@ export function initMysteryEncounters() {
|
|||
allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter;
|
||||
// allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = Abs;
|
||||
allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = AbsoluteAvariceEncounter;
|
||||
|
||||
// Add extreme encounters to biome map
|
||||
extremeBiomeEncounters.forEach(encounter => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { BattlerIndex, BattleType } from "#app/battle";
|
||||
import { biomeLinks } from "#app/data/biomes";
|
||||
import { biomeLinks, BiomePoolTier } from "#app/data/biomes";
|
||||
import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
|
||||
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier";
|
||||
import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
|
||||
import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
|
||||
import * as Overrides from "#app/overrides";
|
||||
import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, MovePhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases";
|
||||
import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases";
|
||||
|
@ -54,7 +54,7 @@ export function doTrainerExclamation(scene: BattleScene) {
|
|||
}
|
||||
});
|
||||
|
||||
scene.playSound("GEN8- Exclaim.wav", { volume: 0.7 });
|
||||
scene.playSound("GEN8- Exclaim", { volume: 0.7 });
|
||||
}
|
||||
|
||||
export interface EnemyPokemonConfig {
|
||||
|
@ -344,20 +344,10 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M
|
|||
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier);
|
||||
let result: ModifierType = modifierTypes[modifierId]?.();
|
||||
|
||||
// Gets tier of item by checking player item pool
|
||||
const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER);
|
||||
Object.keys(modifierPool).every(modifierTier => {
|
||||
const modType = modifierPool[modifierTier].find(m => {
|
||||
if (m.modifierType.id === modifierId) {
|
||||
return m;
|
||||
}
|
||||
});
|
||||
if (modType) {
|
||||
result = modType.modifierType;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Populates item id and tier (order matters)
|
||||
result = result
|
||||
.withIdFromFunc(modifierTypes[modifierId])
|
||||
.withTierFromPool();
|
||||
|
||||
result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result;
|
||||
return new ModifierTypeOption(result, 0);
|
||||
|
@ -881,3 +871,72 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n
|
|||
|
||||
console.log(stats);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TODO: remove once encounter spawn rate is finalized
|
||||
* Just a helper function to calculate aggregate stats for MEs in a Classic run
|
||||
* @param scene
|
||||
* @param luckValue - 0 to 14
|
||||
*/
|
||||
export function calculateRareSpawnAggregateStats(scene: BattleScene, luckValue: number) {
|
||||
const numRuns = 1000;
|
||||
let run = 0;
|
||||
|
||||
const calculateNumRareEncounters = (): any[] => {
|
||||
const bossEncountersByRarity = [0, 0, 0, 0];
|
||||
scene.setSeed(Utils.randomString(24));
|
||||
scene.resetSeed();
|
||||
// There are 12 wild boss floors
|
||||
for (let i = 0; i < 12; i++) {
|
||||
// Roll boss tier
|
||||
// luck influences encounter rarity
|
||||
let luckModifier = 0;
|
||||
if (!isNaN(luckValue)) {
|
||||
luckModifier = luckValue * 0.5;
|
||||
}
|
||||
const tierValue = Utils.randSeedInt(64 - luckModifier);
|
||||
const tier = tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE;
|
||||
|
||||
switch (tier) {
|
||||
default:
|
||||
case BiomePoolTier.BOSS:
|
||||
++bossEncountersByRarity[0];
|
||||
break;
|
||||
case BiomePoolTier.BOSS_RARE:
|
||||
++bossEncountersByRarity[1];
|
||||
break;
|
||||
case BiomePoolTier.BOSS_SUPER_RARE:
|
||||
++bossEncountersByRarity[2];
|
||||
break;
|
||||
case BiomePoolTier.BOSS_ULTRA_RARE:
|
||||
++bossEncountersByRarity[3];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bossEncountersByRarity;
|
||||
};
|
||||
|
||||
const encounterRuns: number[][] = [];
|
||||
while (run < numRuns) {
|
||||
scene.executeWithSeedOffset(() => {
|
||||
const bossEncountersByRarity = calculateNumRareEncounters();
|
||||
encounterRuns.push(bossEncountersByRarity);
|
||||
}, 1000 * run);
|
||||
run++;
|
||||
}
|
||||
|
||||
const n = encounterRuns.length;
|
||||
// const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b));
|
||||
// const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n;
|
||||
// const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n);
|
||||
const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n;
|
||||
const rareMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n;
|
||||
const superRareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n;
|
||||
const ultraRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n;
|
||||
|
||||
const stats = `Avg Commons: ${commonMean}\nAvg Rare: ${rareMean}\nAvg Super Rare: ${superRareMean}\nAvg Ultra Rare: ${ultraRareMean}\n`;
|
||||
|
||||
console.log(stats);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { Type } from "#app/data/type";
|
|||
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
|
||||
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
|
||||
export interface MysteryEncounterPokemonData {
|
||||
spriteScale?: number
|
||||
|
@ -233,6 +233,36 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb
|
|||
pokemon.calculateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will attempt to add a new modifier to a Pokemon.
|
||||
* If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified.
|
||||
* @param scene
|
||||
* @param pokemon
|
||||
* @param modType
|
||||
* @param fallbackModifierType
|
||||
*/
|
||||
export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon, modType: PokemonHeldItemModifierType, fallbackModifierType?: PokemonHeldItemModifierType) {
|
||||
// Check if the Pokemon has max stacks of that item already
|
||||
const existing = scene.findModifier(m => (
|
||||
m instanceof PokemonHeldItemModifier &&
|
||||
m.type.id === modType.id &&
|
||||
m.pokemonId === pokemon.id
|
||||
)) as PokemonHeldItemModifier;
|
||||
|
||||
// At max stacks
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
if (!fallbackModifierType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply fallback
|
||||
return applyModifierTypeToPlayerPokemon(scene, pokemon, fallbackModifierType);
|
||||
}
|
||||
|
||||
const modifier = modType.newModifier(pokemon);
|
||||
await scene.addModifier(modifier, false, false, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to using AttemptCapturePhase
|
||||
* Assumes player sprite is visible on the screen (this is intended for non-combat uses)
|
||||
|
@ -407,7 +437,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number,
|
|||
});
|
||||
}
|
||||
|
||||
export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType, isObtain: boolean = false): Promise<void> {
|
||||
export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType, showCatchObtainMessage: boolean = true, isObtain: boolean = false): Promise<void> {
|
||||
scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY));
|
||||
|
||||
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();
|
||||
|
@ -433,7 +463,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
|
|||
scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
return new Promise(resolve => {
|
||||
scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => {
|
||||
const doPokemonCatchMenu = () => {
|
||||
const end = () => {
|
||||
scene.pokemonInfoContainer.hide();
|
||||
removePb(scene, pokeball);
|
||||
|
@ -488,7 +518,13 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
|
|||
addToParty();
|
||||
}
|
||||
});
|
||||
}, 0, true);
|
||||
};
|
||||
|
||||
if (showCatchObtainMessage) {
|
||||
scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, doPokemonCatchMenu, 0, true);
|
||||
} else {
|
||||
doPokemonCatchMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
|
|||
}
|
||||
}
|
||||
|
||||
if (alpha) {
|
||||
if (!isNaN(alpha)) {
|
||||
sprite.setAlpha(alpha);
|
||||
tintSprite.setAlpha(alpha);
|
||||
}
|
||||
|
@ -289,6 +289,22 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Sprite/TintSprite pair
|
||||
* @param index
|
||||
*/
|
||||
getSpriteAtIndex(index: number): Phaser.GameObjects.Sprite[] {
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret: Phaser.GameObjects.Sprite[] = [];
|
||||
ret.push(this.getAt(index * 2)); // Sprite
|
||||
ret.push(this.getAt(index * 2 + 1)); // Tint Sprite
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
getSprites(): Phaser.GameObjects.Sprite[] {
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
|
|
|
@ -14,6 +14,7 @@ import { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-s
|
|||
import { pokemonSalesmanDialogue } from "#app/locales/en/mystery-encounters/pokemon-salesman-dialogue";
|
||||
import { offerYouCantRefuseDialogue } from "#app/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue";
|
||||
import { delibirdyDialogue } from "#app/locales/en/mystery-encounters/delibirdy-dialogue";
|
||||
import { absoluteAvariceDialogue } from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue";
|
||||
|
||||
/**
|
||||
* Patterns that can be used:
|
||||
|
@ -53,4 +54,5 @@ export const mysteryEncounter = {
|
|||
pokemonSalesman: pokemonSalesmanDialogue,
|
||||
offerYouCantRefuse: offerYouCantRefuseDialogue,
|
||||
delibirdy: delibirdyDialogue,
|
||||
absoluteAvarice: absoluteAvariceDialogue,
|
||||
} as const;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
export const absoluteAvariceDialogue = {
|
||||
intro: "A Greedent ambushed you\nand stole 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?",
|
||||
option: {
|
||||
1: {
|
||||
label: "Battle It",
|
||||
tooltip: "(-) Tough Battle\n(+) Rewards from its Berry Hoard",
|
||||
selected: "The Greedent stuffs its cheeks\nand prepares for battle!",
|
||||
boss_enraged: "Greedent's fierce love for food has it incensed!",
|
||||
food_stash: `It looks like the Greedent was guarding an enormous stash of food!
|
||||
$@s{item_fanfare}Each Pokémon in your party gains 1x Reviver Seed!`
|
||||
},
|
||||
2: {
|
||||
label: "Reason with It",
|
||||
tooltip: "(+) Regain Some Lost Berries",
|
||||
selected: `Your pleading strikes a chord with the Greedent.
|
||||
$It doesn't give all your berries back, but still tosses a few in your direction.`,
|
||||
},
|
||||
3: {
|
||||
label: "Let It Have the Food",
|
||||
tooltip: "(-) Lose All Berries\n(?) The Greedent Will Like You",
|
||||
selected: `The Greedent devours the entire\nstash of berries in a flash!
|
||||
$Patting its stomach,\nit looks at you appreciatively.
|
||||
$Perhaps you could feed it\nmore berries on your adventure...
|
||||
$@s{level_up_fanfare}The Greedent wants to join your party!`,
|
||||
},
|
||||
}
|
||||
};
|
|
@ -5,20 +5,20 @@ export const fieryFalloutDialogue = {
|
|||
query: "What will you do?",
|
||||
option: {
|
||||
1: {
|
||||
label: "Find the source",
|
||||
label: "Find the Source",
|
||||
tooltip: "(?) Discover the source\n(-) Hard Battle",
|
||||
selected: `You push through the storm, and find two Volcarona in the middle of a mating dance!
|
||||
$They don't take kindly to the interruption and attack!`
|
||||
},
|
||||
2: {
|
||||
label: "Hunker down",
|
||||
label: "Hunker Down",
|
||||
tooltip: "(-) Suffer the effects of the weather",
|
||||
selected: `The weather effects cause significant\nharm as you struggle to find shelter!
|
||||
$Your party takes 20% Max HP damage!`,
|
||||
target_burned: "Your {{burnedPokemon}} also became burned!"
|
||||
},
|
||||
3: {
|
||||
label: "Your Fire types help",
|
||||
label: "Your Fire Types Help",
|
||||
tooltip: "(+) End the conditions\n(+) Gain a Charcoal",
|
||||
disabled_tooltip: "You need at least 2 Fire Type Pokémon to choose this",
|
||||
selected: `Your {{option3PrimaryName}} and {{option3SecondaryName}} guide you to where two Volcarona are in the middle of a mating dance!
|
||||
|
|
|
@ -5,7 +5,7 @@ export const lostAtSeaDialogue = {
|
|||
query: "What will you do?",
|
||||
option: {
|
||||
1: {
|
||||
label: "{{option1PrimaryName}} can help",
|
||||
label: "{{option1PrimaryName}} Might Help",
|
||||
label_disabled: "Can't {{option1RequiredMove}}",
|
||||
tooltip: "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP",
|
||||
tooltip_disabled: "You have no Pokémon to {{option1RequiredMove}} on",
|
||||
|
@ -13,7 +13,7 @@ export const lostAtSeaDialogue = {
|
|||
\${{option1PrimaryName}} seems to also have gotten stronger in this time of need!`,
|
||||
},
|
||||
2: {
|
||||
label: "{{option2PrimaryName}} can help",
|
||||
label: "{{option2PrimaryName}} Might Help",
|
||||
label_disabled: "Can't {{option2RequiredMove}}",
|
||||
tooltip: "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP",
|
||||
tooltip_disabled: "You have no Pokémon to {{option2RequiredMove}} with",
|
||||
|
@ -21,7 +21,7 @@ export const lostAtSeaDialogue = {
|
|||
\${{option2PrimaryName}} seems to also have gotten stronger in this time of need!`,
|
||||
},
|
||||
3: {
|
||||
label: "Wander aimlessly",
|
||||
label: "Wander Aimlessly",
|
||||
tooltip: "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP",
|
||||
selected: `You float about in the boat, steering without direction until you finally spot a landmark you remember.
|
||||
$You and your Pokémon are fatigued from the whole ordeal.`,
|
||||
|
|
|
@ -5,15 +5,15 @@ export const mysteriousChallengersDialogue = {
|
|||
query: "Who will you battle?",
|
||||
option: {
|
||||
1: {
|
||||
label: "A clever, mindful foe",
|
||||
label: "A Clever, Mindful Foe",
|
||||
tooltip: "(-) Standard Battle\n(+) Move Item Rewards",
|
||||
},
|
||||
2: {
|
||||
label: "A strong foe",
|
||||
label: "A Strong Foe",
|
||||
tooltip: "(-) Hard Battle\n(+) Good Rewards",
|
||||
},
|
||||
3: {
|
||||
label: "The mightiest foe",
|
||||
label: "The Mightiest Foe",
|
||||
tooltip: "(-) Brutal Battle\n(+) Great Rewards",
|
||||
},
|
||||
selected: "The trainer steps forward...",
|
||||
|
|
|
@ -5,7 +5,7 @@ export const mysteriousChestDialogue = {
|
|||
query: "Will you open it?",
|
||||
option: {
|
||||
1: {
|
||||
label: "Open it",
|
||||
label: "Open It",
|
||||
tooltip: "@[SUMMARY_BLUE]{(35%) Something terrible}\n@[SUMMARY_GREEN]{(40%) Okay Rewards}\n@[SUMMARY_GREEN]{(20%) Good Rewards}\n@[SUMMARY_GREEN]{(4%) Great Rewards}\n@[SUMMARY_GREEN]{(1%) Amazing Rewards}",
|
||||
selected: "You open the chest to find...",
|
||||
normal: "Just some normal tools and items.",
|
||||
|
@ -16,7 +16,7 @@ export const mysteriousChestDialogue = {
|
|||
$Your {{pokeName}} jumps in front of you\nbut is KOed in the process.`,
|
||||
},
|
||||
2: {
|
||||
label: "It's too risky, leave",
|
||||
label: "Too Risky, Leave",
|
||||
tooltip: "(-) No Rewards",
|
||||
selected: "You hurry along your way,\nwith a slight feeling of regret.",
|
||||
},
|
||||
|
|
|
@ -22,12 +22,12 @@ export const safariZoneDialogue = {
|
|||
selected: "You throw a Pokéball!",
|
||||
},
|
||||
2: {
|
||||
label: "Throw bait",
|
||||
label: "Throw Bait",
|
||||
tooltip: "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate",
|
||||
selected: "You throw some bait!",
|
||||
},
|
||||
3: {
|
||||
label: "Throw mud",
|
||||
label: "Throw Mud",
|
||||
tooltip: "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate",
|
||||
selected: "You throw some mud!",
|
||||
},
|
||||
|
|
|
@ -6,19 +6,19 @@ export const slumberingSnorlaxDialogue = {
|
|||
query: "What will you do?",
|
||||
option: {
|
||||
1: {
|
||||
label: "Battle it",
|
||||
label: "Battle It",
|
||||
tooltip: "(-) Fight Sleeping Snorlax\n(+) Special Reward",
|
||||
selected: "You approach the\nPokémon without fear.",
|
||||
},
|
||||
2: {
|
||||
label: "Wait for it to move",
|
||||
label: "Wait for It to Move",
|
||||
tooltip: "(-) Wait a Long Time\n(+) Recover Party",
|
||||
selected: `.@d{32}.@d{32}.@d{32}
|
||||
$You wait for a time, but the Snorlax's yawns make your party sleepy...`,
|
||||
rest_result: "When you all awaken, the Snorlax is no where to be found -\nbut your Pokémon are all healed!",
|
||||
},
|
||||
3: {
|
||||
label: "Steal its item",
|
||||
label: "Steal Its Item",
|
||||
tooltip: "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward",
|
||||
disabled_tooltip: "Your Pokémon need to know certain moves to choose this",
|
||||
selected: `Your {{option3PrimaryName}} uses {{option3PrimaryMove}}!
|
||||
|
|
|
@ -5,7 +5,7 @@ export const theStrongStuffDialogue = {
|
|||
query: "What will you do?",
|
||||
option: {
|
||||
1: {
|
||||
label: "Let it touch you",
|
||||
label: "Let It Touch You",
|
||||
tooltip: "(?) Something awful or amazing might happen",
|
||||
selected: "You black out.",
|
||||
selected_2: `@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained.
|
||||
|
|
|
@ -109,11 +109,35 @@ export class ModifierType {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates item id for ModifierType instance
|
||||
* @param func
|
||||
*/
|
||||
withIdFromFunc(func: ModifierTypeFunc): ModifierType {
|
||||
this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates item tier for ModifierType instance
|
||||
* Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use)
|
||||
* To find the tier, this function performs a reverse lookup of the item type in modifier pools
|
||||
* @param poolType - Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from
|
||||
*/
|
||||
withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType {
|
||||
const modifierPool = getModifierPoolForType(poolType);
|
||||
Object.values(modifierPool).every(weightedModifiers => {
|
||||
weightedModifiers.every(m => {
|
||||
if (m.modifierType.id === this.id) {
|
||||
this.tier = m.modifierType.tier;
|
||||
return false; // Early lookup return if tier found
|
||||
}
|
||||
});
|
||||
return !this.tier; // Early lookup return if tier found
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
newModifier(...args: any[]): Modifier {
|
||||
return this.newModifierFunc(this, args);
|
||||
}
|
||||
|
@ -1841,6 +1865,21 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc {
|
|||
return modifierTypes[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates modifier options for a SelectModifierPhase
|
||||
* @param count - Determines the number of items to generate
|
||||
* @param party - Party is required for generating proper modifier pools
|
||||
* @param modifierTiers - (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule.
|
||||
* @param customModifierSettings - (Optional) If specified, can customize the item shop rewards further.
|
||||
* - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` - If specified, will override the first X items to be specific modifier options (these should be pre-genned).
|
||||
* - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` - If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned).
|
||||
* - `guaranteedModifierTiers?: ModifierTier[]` - If specified, will override the next X items to be the specified tier. These can upgrade with luck.
|
||||
* - `fillRemaining?: boolean` - Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value.
|
||||
* - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`,
|
||||
* - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally.
|
||||
* - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value).
|
||||
* - `rerollMultiplier?: number` - If specified, can adjust the amount of money required for a shop reroll. If set to 0, the shop will not allow rerolls at all.
|
||||
*/
|
||||
export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] {
|
||||
const options: ModifierTypeOption[] = [];
|
||||
const retryCount = Math.min(count * 5, 50);
|
||||
|
@ -1849,32 +1888,21 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
|
|||
options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined));
|
||||
});
|
||||
} else {
|
||||
// Guaranteed mods first
|
||||
if (customModifierSettings?.guaranteedModifierTypeOptions?.length) {
|
||||
customModifierSettings?.guaranteedModifierTypeOptions.forEach((option) => {
|
||||
options.push(option);
|
||||
});
|
||||
// Guaranteed mod options first
|
||||
if (customModifierSettings?.guaranteedModifierTypeOptions?.length > 0) {
|
||||
options.push(...customModifierSettings.guaranteedModifierTypeOptions);
|
||||
}
|
||||
|
||||
// Guaranteed mod funcs second
|
||||
if (customModifierSettings?.guaranteedModifierTypeFuncs?.length) {
|
||||
// Guaranteed mod functions second
|
||||
if (customModifierSettings?.guaranteedModifierTypeFuncs?.length > 0) {
|
||||
customModifierSettings?.guaranteedModifierTypeFuncs.forEach((mod, i) => {
|
||||
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod);
|
||||
let guaranteedMod: ModifierType = modifierTypes[modifierId]?.();
|
||||
|
||||
// Gets tier of item by checking player item pool
|
||||
Object.keys(modifierPool).every(modifierTier => {
|
||||
const modType = modifierPool[modifierTier].find(m => {
|
||||
if (m.modifierType.id === modifierId) {
|
||||
return m;
|
||||
}
|
||||
});
|
||||
if (modType) {
|
||||
guaranteedMod = modType.modifierType;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Populates item id and tier
|
||||
guaranteedMod = guaranteedMod
|
||||
.withIdFromFunc(modifierTypes[modifierId])
|
||||
.withTierFromPool();
|
||||
|
||||
const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod;
|
||||
const option = new ModifierTypeOption(modType, 0);
|
||||
|
@ -1883,7 +1911,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
|
|||
}
|
||||
|
||||
// Guaranteed tiers third
|
||||
if (customModifierSettings?.guaranteedModifierTiers?.length) {
|
||||
if (customModifierSettings?.guaranteedModifierTiers?.length > 0) {
|
||||
customModifierSettings?.guaranteedModifierTiers.forEach((tier) => {
|
||||
options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier));
|
||||
});
|
||||
|
@ -1900,8 +1928,12 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
|
|||
// OVERRIDE IF NECESSARY
|
||||
if (Overrides.ITEM_REWARD_OVERRIDE?.length) {
|
||||
options.forEach((mod, i) => {
|
||||
// @ts-ignore: keeps throwing don't use string as index error in typedoc run
|
||||
const override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.();
|
||||
let override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.();
|
||||
// Populates item id and tier
|
||||
override = override
|
||||
.withIdFromFunc(modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]])
|
||||
.withTierFromPool();
|
||||
|
||||
mod.type = (override instanceof ModifierTypeGenerator ? override.generateType(party) : override) || mod.type;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { VoucherType } from "../system/voucher";
|
|||
import { FormChangeItem, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms";
|
||||
import { Nature } from "#app/data/nature";
|
||||
import * as Overrides from "../overrides";
|
||||
import { ModifierType, modifierTypes } from "./modifier-type";
|
||||
import { ModifierType, ModifierTypeGenerator, modifierTypes } from "./modifier-type";
|
||||
import { Command } from "#app/ui/command-ui-handler.js";
|
||||
import { Species } from "#enums/species";
|
||||
import i18next from "i18next";
|
||||
|
@ -2683,8 +2683,12 @@ export function overrideModifiers(scene: BattleScene, player: boolean = true): v
|
|||
if (!modifierTypes.hasOwnProperty(modifierName)) {
|
||||
return;
|
||||
} // if the modifier does not exist, we skip it
|
||||
const modifierType: ModifierType = modifierTypes[modifierName]();
|
||||
const modifier: PersistentModifier = modifierType.withIdFromFunc(modifierTypes[modifierName]).newModifier() as PersistentModifier;
|
||||
let modifierType: ModifierType = modifierTypes[modifierName]();
|
||||
if (modifierType instanceof ModifierTypeGenerator) {
|
||||
modifierType = (modifierType as ModifierTypeGenerator).generateType(scene.getParty(), [item.type]);
|
||||
}
|
||||
modifierType = modifierType.withIdFromFunc(modifierTypes[modifierName]);
|
||||
const modifier: PersistentModifier = modifierType.newModifier(scene.getParty()[0]) as PersistentModifier;
|
||||
modifier.stackCount = qty;
|
||||
if (player) {
|
||||
scene.addModifier(modifier, true, false, false, true);
|
||||
|
|
|
@ -137,7 +137,7 @@ export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null;
|
|||
* - BerryType is for BERRY
|
||||
* - SpeciesStatBoosterItem is for SPECIES_STAT_BOOSTER
|
||||
*/
|
||||
interface ModifierOverride {
|
||||
export interface ModifierOverride {
|
||||
name: keyof typeof modifierTypes & string,
|
||||
count?: integer
|
||||
type?: TempBattleStat|Stat|Nature|Type|BerryType|SpeciesStatBoosterItem
|
||||
|
@ -155,4 +155,4 @@ export const NEVER_CRIT_OVERRIDE: boolean = false;
|
|||
* If less items are listed than rolled, only some items will be replaced
|
||||
* If more items are listed than rolled, only the first X items will be shown, where X is the number of items rolled.
|
||||
*/
|
||||
export const ITEM_REWARD_OVERRIDE: Array<String> = [];
|
||||
export const ITEM_REWARD_OVERRIDE: Array<keyof typeof modifierTypes & string> = [];
|
||||
|
|
|
@ -903,7 +903,7 @@ export class EncounterPhase extends BattlePhase {
|
|||
loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter.introVisuals.initSprite()));
|
||||
// Load Mystery Encounter Exclamation bubble and sfx
|
||||
loadEnemyAssets.push(new Promise<void>(resolve => {
|
||||
this.scene.loadSe("GEN8- Exclaim.wav", "battle_anims", "GEN8- Exclaim.wav");
|
||||
this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav");
|
||||
this.scene.loadAtlas("exclaim", "mystery-encounters");
|
||||
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve());
|
||||
if (!this.scene.load.isLoading()) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Button } from "#app/enums/buttons";
|
||||
import { CommandPhase, MessagePhase, VictoryPhase } from "#app/phases";
|
||||
import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases";
|
||||
import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases";
|
||||
import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import GameManager from "../utils/gameManager";
|
||||
|
@ -26,29 +26,39 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
|
|||
game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
|
||||
// If a battle is started, fast forward to end of the battle
|
||||
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
|
||||
game.scene.clearPhaseQueue();
|
||||
game.scene.clearPhaseQueueSplice();
|
||||
game.scene.unshiftPhase(new VictoryPhase(game.scene, 0));
|
||||
game.endPhase();
|
||||
});
|
||||
|
||||
// Handle end of battle trainer messages
|
||||
game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
|
||||
// Handle egg hatch dialogue
|
||||
game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
}, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase));
|
||||
|
||||
if (isBattle) {
|
||||
game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
|
||||
game.setMode(Mode.MESSAGE);
|
||||
game.endPhase();
|
||||
}, () => game.isCurrentPhase(CommandPhase));
|
||||
|
||||
game.onNextPrompt("CheckSwitchPhase", Mode.MESSAGE, () => {
|
||||
game.setMode(Mode.MESSAGE);
|
||||
game.endPhase();
|
||||
}, () => game.isCurrentPhase(CommandPhase));
|
||||
|
||||
// If a battle is started, fast forward to end of the battle
|
||||
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
|
||||
game.scene.clearPhaseQueue();
|
||||
game.scene.clearPhaseQueueSplice();
|
||||
game.scene.unshiftPhase(new VictoryPhase(game.scene, 0));
|
||||
game.endPhase();
|
||||
});
|
||||
|
||||
// Handle end of battle trainer messages
|
||||
game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
|
||||
// Handle egg hatch dialogue
|
||||
game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
} else {
|
||||
await game.phaseInterceptor.to(MysteryEncounterRewardsPhase);
|
||||
|
@ -60,7 +70,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN
|
|||
game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
}, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase));
|
||||
|
||||
if (game.isCurrentPhase(MessagePhase)) {
|
||||
await game.phaseInterceptor.run(MessagePhase);
|
||||
|
@ -70,7 +80,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN
|
|||
game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => {
|
||||
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
|
||||
uiHandler.processInput(Button.ACTION);
|
||||
});
|
||||
}, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase));
|
||||
|
||||
await game.phaseInterceptor.to(MysteryEncounterPhase, true);
|
||||
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
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, 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 { BerryModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter";
|
||||
import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases";
|
||||
import { Moves } from "#enums/moves";
|
||||
|
||||
const namespace = "mysteryEncounter:absoluteAvarice";
|
||||
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
|
||||
const defaultBiome = Biome.PLAINS;
|
||||
const defaultWave = 45;
|
||||
|
||||
describe("Absolute Avarice - Mystery Encounter", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let scene: BattleScene;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
game = new GameManager(phaserGame);
|
||||
scene = game.scene;
|
||||
game.override.mysteryEncounterChance(100);
|
||||
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
|
||||
game.override.startingWave(defaultWave);
|
||||
game.override.startingBiome(defaultBiome);
|
||||
|
||||
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
|
||||
new Map<Biome, MysteryEncounterType[]>([
|
||||
[Biome.PLAINS, [MysteryEncounterType.ABSOLUTE_AVARICE]],
|
||||
[Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should have the correct properties", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
|
||||
expect(AbsoluteAvariceEncounter.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
expect(AbsoluteAvariceEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT);
|
||||
expect(AbsoluteAvariceEncounter.dialogue).toBeDefined();
|
||||
expect(AbsoluteAvariceEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}:intro` }]);
|
||||
expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`);
|
||||
expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`);
|
||||
expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`);
|
||||
expect(AbsoluteAvariceEncounter.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.ABSOLUTE_AVARICE);
|
||||
});
|
||||
|
||||
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.startingBiome(Biome.VOLCANO);
|
||||
await game.runToMysteryEncounter();
|
||||
|
||||
expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
});
|
||||
|
||||
it("should not spawn if player does not have enough berries", async () => {
|
||||
scene.modifiers = [];
|
||||
|
||||
await game.runToMysteryEncounter();
|
||||
|
||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
});
|
||||
|
||||
it("should spawn if player has enough berries", async () => {
|
||||
game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]);
|
||||
|
||||
await game.runToMysteryEncounter();
|
||||
|
||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
});
|
||||
|
||||
it("should remove all player's berries at the start of the encounter", async () => {
|
||||
game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]);
|
||||
|
||||
await game.runToMysteryEncounter();
|
||||
|
||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
expect(scene.modifiers?.length).toBe(0);
|
||||
});
|
||||
|
||||
describe("Option 1 - Fight the Greedent", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option1 = AbsoluteAvariceEncounter.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 Greedent", async () => {
|
||||
const phaseSpy = vi.spyOn(scene, "pushPhase");
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, 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.GREEDENT);
|
||||
const moveset = enemyField[0].moveset.map(m => m.moveId);
|
||||
expect(moveset?.length).toBe(4);
|
||||
expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]);
|
||||
|
||||
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.STUFF_CHEEKS).length).toBe(1); // Stuff Cheeks used before battle
|
||||
});
|
||||
|
||||
it("should give reviver seed to each pokemon after battle", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 1, null, true);
|
||||
await skipBattleRunMysteryEncounterRewardsPhase(game);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
|
||||
|
||||
for (const partyPokemon of scene.getParty()) {
|
||||
const pokemonId = partyPokemon.id;
|
||||
const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
||||
&& (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[];
|
||||
const revSeed = pokemonItems.find(i => i.type.name === "Reviver Seed");
|
||||
expect(revSeed).toBeDefined;
|
||||
expect(revSeed.stackCount).toBe(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Option 2 - Reason with It", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option = AbsoluteAvariceEncounter.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 return 3 (2/5ths floored) berries if 8 were stolen", async () => {
|
||||
game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 3, type: BerryType.APICOT}]);
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
|
||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
expect(scene.modifiers?.length).toBe(0);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 2);
|
||||
|
||||
const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier);
|
||||
const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0);
|
||||
expect(berriesAfter).toBeDefined();
|
||||
expect(berryCountAfter).toBe(3);
|
||||
});
|
||||
|
||||
it("Should return 2 (2/5ths floored) berries if 7 were stolen", async () => {
|
||||
game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 2, type: BerryType.APICOT}]);
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
|
||||
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
|
||||
expect(scene.modifiers?.length).toBe(0);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 2);
|
||||
|
||||
const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier);
|
||||
const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0);
|
||||
expect(berriesAfter).toBeDefined();
|
||||
expect(berryCountAfter).toBe(2);
|
||||
});
|
||||
|
||||
it("should leave encounter without battle", async () => {
|
||||
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 2);
|
||||
|
||||
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Option 3 - Let it have the food", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option = AbsoluteAvariceEncounter.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 add Greedent to the party", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
const partyCountBefore = scene.getParty().length;
|
||||
|
||||
await runMysteryEncounterToEnd(game, 3);
|
||||
const partyCountAfter = scene.getParty().length;
|
||||
|
||||
expect(partyCountBefore + 1).toBe(partyCountAfter);
|
||||
const greedent = scene.getParty()[scene.getParty().length - 1];
|
||||
expect(greedent.species.speciesId).toBe(Species.GREEDENT);
|
||||
const moveset = greedent.moveset.map(m => m.moveId);
|
||||
expect(moveset?.length).toBe(4);
|
||||
expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]);
|
||||
});
|
||||
|
||||
it("should leave encounter without battle", async () => {
|
||||
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 3);
|
||||
|
||||
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,7 +11,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
|||
import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter";
|
||||
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
|
||||
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
||||
import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } from "#app/modifier/modifier";
|
||||
import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } from "#app/modifier/modifier";
|
||||
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
|
||||
import { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
|
@ -131,6 +131,28 @@ describe("Delibird-y - Mystery Encounter", () => {
|
|||
expect(itemModifier.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
|
||||
scene.money = 200000;
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// 5 Healing Charms
|
||||
scene.modifiers = [];
|
||||
const abilityCharm = generateModifierTypeOption(scene, modifierTypes.ABILITY_CHARM).type.newModifier() as HiddenAbilityRateBoosterModifier;
|
||||
abilityCharm.stackCount = 4;
|
||||
await scene.addModifier(abilityCharm, true, false, false, true);
|
||||
await scene.updateModifiers(true);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
|
||||
const abilityCharmAfter = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier);
|
||||
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
|
||||
|
||||
expect(abilityCharmAfter).toBeDefined();
|
||||
expect(abilityCharmAfter.stackCount).toBe(4);
|
||||
expect(shellBellAfter).toBeDefined();
|
||||
expect(shellBellAfter.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should be disabled if player does not have enough money", async () => {
|
||||
scene.money = 0;
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
@ -221,6 +243,64 @@ describe("Delibird-y - Mystery Encounter", () => {
|
|||
expect(healingCharmAfter.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// 99 Candy Jars
|
||||
scene.modifiers = [];
|
||||
const candyJar = generateModifierTypeOption(scene, modifierTypes.CANDY_JAR).type.newModifier() as LevelIncrementBoosterModifier;
|
||||
candyJar.stackCount = 99;
|
||||
await scene.addModifier(candyJar, true, false, false, true);
|
||||
const sitrus = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type;
|
||||
|
||||
// Sitrus berries on party
|
||||
const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier;
|
||||
sitrusMod.stackCount = 2;
|
||||
await scene.addModifier(sitrusMod, true, false, false, true);
|
||||
await scene.updateModifiers(true);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1});
|
||||
|
||||
const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier);
|
||||
const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier);
|
||||
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
|
||||
|
||||
expect(sitrusAfter.stackCount).toBe(1);
|
||||
expect(candyJarAfter).toBeDefined();
|
||||
expect(candyJarAfter.stackCount).toBe(99);
|
||||
expect(shellBellAfter).toBeDefined();
|
||||
expect(shellBellAfter.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// 5 Healing Charms
|
||||
scene.modifiers = [];
|
||||
const healingCharm = generateModifierTypeOption(scene, modifierTypes.HEALING_CHARM).type.newModifier() as HealingBoosterModifier;
|
||||
healingCharm.stackCount = 5;
|
||||
await scene.addModifier(healingCharm, true, false, false, true);
|
||||
|
||||
// Set 1 Reviver Seed on party lead
|
||||
const revSeed = generateModifierTypeOption(scene, modifierTypes.REVIVER_SEED).type;
|
||||
const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier;
|
||||
modifier.stackCount = 1;
|
||||
await scene.addModifier(modifier, true, false, false, true);
|
||||
await scene.updateModifiers(true);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1});
|
||||
|
||||
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
|
||||
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
|
||||
|
||||
expect(reviverSeedAfter).toBeUndefined();
|
||||
expect(healingCharmAfter).toBeDefined();
|
||||
expect(healingCharmAfter.stackCount).toBe(5);
|
||||
expect(shellBellAfter).toBeDefined();
|
||||
expect(shellBellAfter.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should be disabled if player does not have any proper items", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
|
@ -325,6 +405,35 @@ describe("Delibird-y - Mystery Encounter", () => {
|
|||
expect(berryPouchAfter.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// 5 Healing Charms
|
||||
scene.modifiers = [];
|
||||
const healingCharm = generateModifierTypeOption(scene, modifierTypes.BERRY_POUCH).type.newModifier() as PreserveBerryModifier;
|
||||
healingCharm.stackCount = 3;
|
||||
await scene.addModifier(healingCharm, true, false, false, true);
|
||||
|
||||
// Set 1 Soul Dew on party lead
|
||||
const soulDew = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type;
|
||||
const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier;
|
||||
modifier.stackCount = 1;
|
||||
await scene.addModifier(modifier, true, false, false, true);
|
||||
await scene.updateModifiers(true);
|
||||
|
||||
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1});
|
||||
|
||||
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
|
||||
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
|
||||
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
|
||||
|
||||
expect(soulDewAfter).toBeUndefined();
|
||||
expect(berryPouchAfter).toBeDefined();
|
||||
expect(berryPouchAfter.stackCount).toBe(3);
|
||||
expect(shellBellAfter).toBeDefined();
|
||||
expect(shellBellAfter.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("should be disabled if player does not have any proper items", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import * as overrides from "#app/overrides";
|
|||
import * as GameMode from "#app/game-mode";
|
||||
import { GameModes, getGameMode } from "#app/game-mode";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { ModifierOverride } from "#app/overrides";
|
||||
|
||||
/**
|
||||
* Helper to handle overrides in tests
|
||||
|
@ -117,6 +118,12 @@ export class OverridesHelper {
|
|||
return spy;
|
||||
}
|
||||
|
||||
starterHeldItems(modifiers: ModifierOverride[]) {
|
||||
const spy = vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers);
|
||||
this.log(`Starting modifiers set to ${modifiers.map(m => JSON.stringify(m)).join(", ")}!`);
|
||||
return spy;
|
||||
}
|
||||
|
||||
private log(...params: any[]) {
|
||||
console.log("Overrides:", ...params);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,14 @@ import {
|
|||
PostMysteryEncounterPhase
|
||||
} from "#app/phases/mystery-encounter-phases";
|
||||
|
||||
export interface PromptHandler {
|
||||
phaseTarget?;
|
||||
mode?;
|
||||
callback?;
|
||||
expireFn?;
|
||||
awaitingActionInput?;
|
||||
}
|
||||
|
||||
export default class PhaseInterceptor {
|
||||
public scene;
|
||||
public phases = {};
|
||||
|
@ -56,7 +64,7 @@ export default class PhaseInterceptor {
|
|||
private interval;
|
||||
private promptInterval;
|
||||
private intervalRun;
|
||||
private prompts;
|
||||
private prompts: PromptHandler[];
|
||||
private phaseFrom;
|
||||
private inProgress;
|
||||
private originalSetMode;
|
||||
|
@ -337,6 +345,7 @@ export default class PhaseInterceptor {
|
|||
* @param mode - The mode of the UI.
|
||||
* @param callback - The callback function to execute.
|
||||
* @param expireFn - The function to determine if the prompt has expired.
|
||||
* @param awaitingActionInput
|
||||
*/
|
||||
addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void, awaitingActionInput: boolean = false) {
|
||||
this.prompts.push({
|
||||
|
|
Loading…
Reference in New Issue