Merge pull request #118 from AsdarDevelops/absolute-avarice

[Event] Absolute Avarice
This commit is contained in:
ImperialSympathizer 2024-07-28 15:25:32 -04:00 committed by GitHub
commit 8b10756644
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1286 additions and 141 deletions

View File

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

View File

@ -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();
}

View File

@ -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
// 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,12 +177,35 @@ 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")) {
// 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 {
// 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
modifier.stackCount -= 1;
@ -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
// 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;

View File

@ -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`);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 => {

View File

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

View File

@ -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();
}
});
}

View File

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

View File

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

View File

@ -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!`,
},
}
};

View File

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

View File

@ -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.`,

View File

@ -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...",

View File

@ -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.",
},

View File

@ -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!",
},

View File

@ -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}}!

View File

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

View File

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

View File

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

View File

@ -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> = [];

View File

@ -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()) {

View File

@ -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,7 +26,18 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
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, () => {
@ -48,7 +59,6 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
uiHandler.processInput(Button.ACTION);
});
if (isBattle) {
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);

View File

@ -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();
});
});
});

View File

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

View File

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

View File

@ -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({