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); this.field.add(newTrainer);
} }
// TODO: remove this once spawn rates are finalized // TODO: remove these once ME spawn rates are finalized
// let testStartingWeight = 0; // let testStartingWeight = 0;
// while (testStartingWeight < 3) { // while (testStartingWeight < 3) {
// calculateMEAggregateStats(this, testStartingWeight); // calculateMEAggregateStats(this, testStartingWeight);
// testStartingWeight += 2; // testStartingWeight += 2;
// } // }
// calculateRareSpawnAggregateStats(this, 14);
// Check for mystery encounter // Check for mystery encounter
// Can only occur in place of a standard wild battle, waves 10-180 // 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) { 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) { while (availableEncounters.length === 0 && tier >= 0) {
availableEncounters = biomeMysteryEncounters availableEncounters = biomeMysteryEncounters
.filter((encounterType) => { .filter((encounterType) => {
if (allMysteryEncounters[encounterType].encounterTier !== tier) { // Encounter is in tier const encounterCandidate = allMysteryEncounters[encounterType];
if (!encounterCandidate) {
return false; 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; return false;
} }
if (!isNullOrUndefined(previousEncounter) && encounterType === previousEncounter) { // Previous encounter was not this one if (!isNullOrUndefined(previousEncounter) && encounterType === previousEncounter) { // Previous encounter was not this one
return false; return false;
} }
if (this.mysteryEncounterData.encounteredEvents?.length > 0 && // Encounter has not exceeded max allowed encounters if (this.mysteryEncounterData.encounteredEvents?.length > 0 && // Encounter has not exceeded max allowed encounters
allMysteryEncounters[encounterType].maxAllowedEncounters > 0 encounterCandidate.maxAllowedEncounters > 0
&& this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === encounterType).length >= allMysteryEncounters[encounterType].maxAllowedEncounters) { && this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === encounterType).length >= encounterCandidate.maxAllowedEncounters) {
return false; return false;
} }
return true; 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 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 { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; 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 { ModifierRewardPhase } from "#app/phases";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; 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 */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounter:delibirdy"; const namespace = "mysteryEncounter:delibirdy";
@ -39,6 +41,10 @@ export const DelibirdyEncounter: IMysteryEncounter =
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(10, 180) .withSceneWaveRangeRequirement(10, 180)
.withSceneRequirement(new MoneyRequirement(0, 2.75)) // Must have enough money for it to spawn at the very least .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([ .withIntroSpriteConfigs([
{ {
spriteKey: Species.DELIBIRD.toString(), spriteKey: Species.DELIBIRD.toString(),
@ -82,7 +88,7 @@ export const DelibirdyEncounter: IMysteryEncounter =
.withOption( .withOption(
new MysteryEncounterOptionBuilder() new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneMoneyRequirement(0, 2.75) .withSceneMoneyRequirement(0, 2.75) // Must have money to spawn
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option:1:label`, buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`, buttonTooltip: `${namespace}:option:1:tooltip`,
@ -99,7 +105,19 @@ export const DelibirdyEncounter: IMysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Give the player an Ability Charm // 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)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM));
}
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -159,12 +177,35 @@ export const DelibirdyEncounter: IMysteryEncounter =
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;
const modifier = encounter.misc.chosenModifier; const modifier = encounter.misc.chosenModifier;
// Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed
if (modifier.type.name.includes("Berry")) { 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)); 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 { } else {
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
} }
}
// Remove the modifier if its stacks go to 0 // Remove the modifier if its stacks go to 0
modifier.stackCount -= 1; modifier.stackCount -= 1;
@ -231,8 +272,19 @@ export const DelibirdyEncounter: IMysteryEncounter =
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;
const modifier = encounter.misc.chosenModifier; 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)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
}
// Remove the modifier if its stacks go to 0 // Remove the modifier if its stacks go to 0
modifier.stackCount -= 1; modifier.stackCount -= 1;

View File

@ -1,6 +1,6 @@
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; 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 { 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 { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
@ -17,7 +17,7 @@ import { WeatherType } from "#app/data/weather";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; 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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; 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 // Give first party pokemon Charcoal for free at end of battle
const leadPokemon = scene.getParty()?.[0]; const leadPokemon = scene.getParty()?.[0];
if (leadPokemon) { if (leadPokemon) {
const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]); const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]).type as AttackTypeBoosterModifierType;
scene.addModifier(charcoal.type.newModifier(leadPokemon), true); applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal);
scene.updateModifiers();
scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name); scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name);
queueEncounterMessage(scene, `${namespace}:found_charcoal`); 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].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(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)); 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`); await showEncounterText(scene, `${namespace}:option:2:bad_result`);

View File

@ -133,7 +133,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter =
// "Catch" purchased pokemon // "Catch" purchased pokemon
const data = new PokemonData(purchasedPokemon); const data = new PokemonData(purchasedPokemon);
data.player = false; 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); leaveEncounterWithoutBattle(scene, true);
}) })

View File

@ -10,7 +10,7 @@ import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { MoneyRequirement } from "../mystery-encounter-requirements"; import { MoneyRequirement } from "../mystery-encounter-requirements";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; 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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -110,10 +110,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
const modifiers = encounter.misc.modifiers; const modifiers = encounter.misc.modifiers;
for (const modType of modifiers) { for (const modType of modifiers) {
const modifier = modType.newModifier(chosenPokemon); await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType);
await scene.addModifier(modifier, true, false, false, true);
} }
scene.updateModifiers(true);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
}) })
@ -195,10 +193,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
const modifiers = encounter.misc.modifiers; const modifiers = encounter.misc.modifiers;
for (const modType of modifiers) { for (const modType of modifiers) {
const modifier = modType.newModifier(chosenPokemon); await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType);
await scene.addModifier(modifier, true, false, false, true);
} }
scene.updateModifiers(true);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
}) })

View File

@ -1,5 +1,4 @@
import { PlayerPokemon } from "#app/field/pokemon"; import { PlayerPokemon } from "#app/field/pokemon";
import { ModifierType } from "#app/modifier/modifier-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
@ -235,28 +234,36 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
} }
export class PersistentModifierRequirement extends EncounterSceneRequirement { export class PersistentModifierRequirement extends EncounterSceneRequirement {
requiredItems?: ModifierType[]; // TODO: not implemented requiredHeldItemModifiers: string[];
constructor(item: ModifierType | ModifierType[]) { minNumberOfItems: number;
constructor(heldItem: string | string[], minNumberOfItems: number = 1) {
super(); super();
this.requiredItems = Array.isArray(item) ? item : [item]; this.minNumberOfItems = minNumberOfItems;
this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem];
} }
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const items = scene.modifiers; const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) {
if (!isNullOrUndefined(items) && this?.requiredItems.length > 0 && this.requiredItems.filter((searchingMod) =>
items.filter((itemInScene) => itemInScene.type.id === searchingMod.id).length > 0).length === 0) {
return false; 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] { getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const requiredItemsInInventory = this.requiredItems.filter((a) => { if (this.requiredHeldItemModifiers.length > 0) {
scene.modifiers.filter((itemInScene) => itemInScene.type.id === a.id).length > 0; return ["requiredItem", this.requiredHeldItemModifiers[0]];
});
if (requiredItemsInInventory.length > 0) {
return ["requiredItem", requiredItemsInInventory[0].name];
} }
return null; return null;
} }

View File

@ -159,7 +159,7 @@ export default class IMysteryEncounter implements IMysteryEncounter {
if (!isNullOrUndefined(encounter)) { if (!isNullOrUndefined(encounter)) {
Object.assign(this, 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 ?? {}; this.dialogue = this.dialogue ?? {};
// Default max is 1 for ROGUE encounters, 3 for others // Default max is 1 for ROGUE encounters, 3 for others
this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3; 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.requirements = this.requirements ? this.requirements : [];
this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false; this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false;
this.autoHideIntroVisuals = !isNullOrUndefined(this.autoHideIntroVisuals) ? this.autoHideIntroVisuals : true; this.autoHideIntroVisuals = !isNullOrUndefined(this.autoHideIntroVisuals) ? this.autoHideIntroVisuals : true;
this.startOfBattleEffects = this.startOfBattleEffects ?? [];
// Reset any dirty flags or encounter data // Reset any dirty flags or encounter data
this.startOfBattleEffectsComplete = false; this.startOfBattleEffectsComplete = false;
this.lockEncounterRewardTiers = true; this.lockEncounterRewardTiers = true;
this.dialogueTokens = {}; this.dialogueTokens = {};
this.enemyPartyConfigs = []; this.enemyPartyConfigs = [];
this.startOfBattleEffects = [];
this.introVisuals = null; this.introVisuals = null;
this.misc = null; this.misc = null;
this.expMultiplier = 1; 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 { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-encounter";
import { OfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/offer-you-cant-refuse-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 { 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 // 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; export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -238,7 +239,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter; allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter;
allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter; allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter;
allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter; allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter;
// allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = Abs; allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = AbsoluteAvariceEncounter;
// Add extreme encounters to biome map // Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => { extremeBiomeEncounters.forEach(encounter => {

View File

@ -1,11 +1,11 @@
import { BattlerIndex, BattleType } from "#app/battle"; 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 MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; 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 { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; 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 * as Overrides from "#app/overrides";
import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, MovePhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, MovePhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases";
import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-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 { export interface EnemyPokemonConfig {
@ -344,20 +344,10 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier);
let result: ModifierType = modifierTypes[modifierId]?.(); let result: ModifierType = modifierTypes[modifierId]?.();
// Gets tier of item by checking player item pool // Populates item id and tier (order matters)
const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); result = result
Object.keys(modifierPool).every(modifierTier => { .withIdFromFunc(modifierTypes[modifierId])
const modType = modifierPool[modifierTier].find(m => { .withTierFromPool();
if (m.modifierType.id === modifierId) {
return m;
}
});
if (modType) {
result = modType.modifierType;
return false;
}
return true;
});
result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result;
return new ModifierTypeOption(result, 0); return new ModifierTypeOption(result, 0);
@ -881,3 +871,72 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n
console.log(stats); 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 PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
export interface MysteryEncounterPokemonData { export interface MysteryEncounterPokemonData {
spriteScale?: number spriteScale?: number
@ -233,6 +233,36 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb
pokemon.calculateStats(); 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 * Alternative to using AttemptCapturePhase
* Assumes player sprite is visible on the screen (this is intended for non-combat uses) * 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)); scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY));
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); 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); scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
return new Promise(resolve => { return new Promise(resolve => {
scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { const doPokemonCatchMenu = () => {
const end = () => { const end = () => {
scene.pokemonInfoContainer.hide(); scene.pokemonInfoContainer.hide();
removePb(scene, pokeball); removePb(scene, pokeball);
@ -488,7 +518,13 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
addToParty(); 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); sprite.setAlpha(alpha);
tintSprite.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[] { getSprites(): Phaser.GameObjects.Sprite[] {
if (!this.spriteConfigs) { if (!this.spriteConfigs) {
return; 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 { pokemonSalesmanDialogue } from "#app/locales/en/mystery-encounters/pokemon-salesman-dialogue";
import { offerYouCantRefuseDialogue } from "#app/locales/en/mystery-encounters/offer-you-cant-refuse-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 { 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: * Patterns that can be used:
@ -53,4 +54,5 @@ export const mysteryEncounter = {
pokemonSalesman: pokemonSalesmanDialogue, pokemonSalesman: pokemonSalesmanDialogue,
offerYouCantRefuse: offerYouCantRefuseDialogue, offerYouCantRefuse: offerYouCantRefuseDialogue,
delibirdy: delibirdyDialogue, delibirdy: delibirdyDialogue,
absoluteAvarice: absoluteAvariceDialogue,
} as const; } 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?", query: "What will you do?",
option: { option: {
1: { 1: {
label: "Find the source", label: "Find the Source",
tooltip: "(?) Discover the source\n(-) Hard Battle", tooltip: "(?) Discover the source\n(-) Hard Battle",
selected: `You push through the storm, and find two Volcarona in the middle of a mating dance! 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!` $They don't take kindly to the interruption and attack!`
}, },
2: { 2: {
label: "Hunker down", label: "Hunker Down",
tooltip: "(-) Suffer the effects of the weather", tooltip: "(-) Suffer the effects of the weather",
selected: `The weather effects cause significant\nharm as you struggle to find shelter! selected: `The weather effects cause significant\nharm as you struggle to find shelter!
$Your party takes 20% Max HP damage!`, $Your party takes 20% Max HP damage!`,
target_burned: "Your {{burnedPokemon}} also became burned!" target_burned: "Your {{burnedPokemon}} also became burned!"
}, },
3: { 3: {
label: "Your Fire types help", label: "Your Fire Types Help",
tooltip: "(+) End the conditions\n(+) Gain a Charcoal", tooltip: "(+) End the conditions\n(+) Gain a Charcoal",
disabled_tooltip: "You need at least 2 Fire Type Pokémon to choose this", 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! 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?", query: "What will you do?",
option: { option: {
1: { 1: {
label: "{{option1PrimaryName}} can help", label: "{{option1PrimaryName}} Might Help",
label_disabled: "Can't {{option1RequiredMove}}", label_disabled: "Can't {{option1RequiredMove}}",
tooltip: "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP", tooltip: "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP",
tooltip_disabled: "You have no Pokémon to {{option1RequiredMove}} on", 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!`, \${{option1PrimaryName}} seems to also have gotten stronger in this time of need!`,
}, },
2: { 2: {
label: "{{option2PrimaryName}} can help", label: "{{option2PrimaryName}} Might Help",
label_disabled: "Can't {{option2RequiredMove}}", label_disabled: "Can't {{option2RequiredMove}}",
tooltip: "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP", tooltip: "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP",
tooltip_disabled: "You have no Pokémon to {{option2RequiredMove}} with", 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!`, \${{option2PrimaryName}} seems to also have gotten stronger in this time of need!`,
}, },
3: { 3: {
label: "Wander aimlessly", label: "Wander Aimlessly",
tooltip: "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", 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. 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.`, $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?", query: "Who will you battle?",
option: { option: {
1: { 1: {
label: "A clever, mindful foe", label: "A Clever, Mindful Foe",
tooltip: "(-) Standard Battle\n(+) Move Item Rewards", tooltip: "(-) Standard Battle\n(+) Move Item Rewards",
}, },
2: { 2: {
label: "A strong foe", label: "A Strong Foe",
tooltip: "(-) Hard Battle\n(+) Good Rewards", tooltip: "(-) Hard Battle\n(+) Good Rewards",
}, },
3: { 3: {
label: "The mightiest foe", label: "The Mightiest Foe",
tooltip: "(-) Brutal Battle\n(+) Great Rewards", tooltip: "(-) Brutal Battle\n(+) Great Rewards",
}, },
selected: "The trainer steps forward...", selected: "The trainer steps forward...",

View File

@ -5,7 +5,7 @@ export const mysteriousChestDialogue = {
query: "Will you open it?", query: "Will you open it?",
option: { option: {
1: { 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}", 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...", selected: "You open the chest to find...",
normal: "Just some normal tools and items.", 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.`, $Your {{pokeName}} jumps in front of you\nbut is KOed in the process.`,
}, },
2: { 2: {
label: "It's too risky, leave", label: "Too Risky, Leave",
tooltip: "(-) No Rewards", tooltip: "(-) No Rewards",
selected: "You hurry along your way,\nwith a slight feeling of regret.", 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!", selected: "You throw a Pokéball!",
}, },
2: { 2: {
label: "Throw bait", label: "Throw Bait",
tooltip: "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", tooltip: "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate",
selected: "You throw some bait!", selected: "You throw some bait!",
}, },
3: { 3: {
label: "Throw mud", label: "Throw Mud",
tooltip: "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", tooltip: "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate",
selected: "You throw some mud!", selected: "You throw some mud!",
}, },

View File

@ -6,19 +6,19 @@ export const slumberingSnorlaxDialogue = {
query: "What will you do?", query: "What will you do?",
option: { option: {
1: { 1: {
label: "Battle it", label: "Battle It",
tooltip: "(-) Fight Sleeping Snorlax\n(+) Special Reward", tooltip: "(-) Fight Sleeping Snorlax\n(+) Special Reward",
selected: "You approach the\nPokémon without fear.", selected: "You approach the\nPokémon without fear.",
}, },
2: { 2: {
label: "Wait for it to move", label: "Wait for It to Move",
tooltip: "(-) Wait a Long Time\n(+) Recover Party", tooltip: "(-) Wait a Long Time\n(+) Recover Party",
selected: `.@d{32}.@d{32}.@d{32} selected: `.@d{32}.@d{32}.@d{32}
$You wait for a time, but the Snorlax's yawns make your party sleepy...`, $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!", rest_result: "When you all awaken, the Snorlax is no where to be found -\nbut your Pokémon are all healed!",
}, },
3: { 3: {
label: "Steal its item", label: "Steal Its Item",
tooltip: "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward", tooltip: "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward",
disabled_tooltip: "Your Pokémon need to know certain moves to choose this", disabled_tooltip: "Your Pokémon need to know certain moves to choose this",
selected: `Your {{option3PrimaryName}} uses {{option3PrimaryMove}}! selected: `Your {{option3PrimaryName}} uses {{option3PrimaryMove}}!

View File

@ -5,7 +5,7 @@ export const theStrongStuffDialogue = {
query: "What will you do?", query: "What will you do?",
option: { option: {
1: { 1: {
label: "Let it touch you", label: "Let It Touch You",
tooltip: "(?) Something awful or amazing might happen", tooltip: "(?) Something awful or amazing might happen",
selected: "You black out.", selected: "You black out.",
selected_2: `@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained. 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; return null;
} }
/**
* Populates item id for ModifierType instance
* @param func
*/
withIdFromFunc(func: ModifierTypeFunc): ModifierType { withIdFromFunc(func: ModifierTypeFunc): ModifierType {
this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func); this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func);
return this; 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 { newModifier(...args: any[]): Modifier {
return this.newModifierFunc(this, args); return this.newModifierFunc(this, args);
} }
@ -1841,6 +1865,21 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc {
return modifierTypes[id]; 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[] { export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] {
const options: ModifierTypeOption[] = []; const options: ModifierTypeOption[] = [];
const retryCount = Math.min(count * 5, 50); 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)); options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined));
}); });
} else { } else {
// Guaranteed mods first // Guaranteed mod options first
if (customModifierSettings?.guaranteedModifierTypeOptions?.length) { if (customModifierSettings?.guaranteedModifierTypeOptions?.length > 0) {
customModifierSettings?.guaranteedModifierTypeOptions.forEach((option) => { options.push(...customModifierSettings.guaranteedModifierTypeOptions);
options.push(option);
});
} }
// Guaranteed mod funcs second // Guaranteed mod functions second
if (customModifierSettings?.guaranteedModifierTypeFuncs?.length) { if (customModifierSettings?.guaranteedModifierTypeFuncs?.length > 0) {
customModifierSettings?.guaranteedModifierTypeFuncs.forEach((mod, i) => { customModifierSettings?.guaranteedModifierTypeFuncs.forEach((mod, i) => {
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod); const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod);
let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); let guaranteedMod: ModifierType = modifierTypes[modifierId]?.();
// Gets tier of item by checking player item pool // Populates item id and tier
Object.keys(modifierPool).every(modifierTier => { guaranteedMod = guaranteedMod
const modType = modifierPool[modifierTier].find(m => { .withIdFromFunc(modifierTypes[modifierId])
if (m.modifierType.id === modifierId) { .withTierFromPool();
return m;
}
});
if (modType) {
guaranteedMod = modType.modifierType;
return false;
}
return true;
});
const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod;
const option = new ModifierTypeOption(modType, 0); const option = new ModifierTypeOption(modType, 0);
@ -1883,7 +1911,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
} }
// Guaranteed tiers third // Guaranteed tiers third
if (customModifierSettings?.guaranteedModifierTiers?.length) { if (customModifierSettings?.guaranteedModifierTiers?.length > 0) {
customModifierSettings?.guaranteedModifierTiers.forEach((tier) => { customModifierSettings?.guaranteedModifierTiers.forEach((tier) => {
options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier)); options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier));
}); });
@ -1900,8 +1928,12 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
// OVERRIDE IF NECESSARY // OVERRIDE IF NECESSARY
if (Overrides.ITEM_REWARD_OVERRIDE?.length) { if (Overrides.ITEM_REWARD_OVERRIDE?.length) {
options.forEach((mod, i) => { options.forEach((mod, i) => {
// @ts-ignore: keeps throwing don't use string as index error in typedoc run let override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.();
const 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; 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 { FormChangeItem, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms";
import { Nature } from "#app/data/nature"; import { Nature } from "#app/data/nature";
import * as Overrides from "../overrides"; 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 { Command } from "#app/ui/command-ui-handler.js";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import i18next from "i18next"; import i18next from "i18next";
@ -2683,8 +2683,12 @@ export function overrideModifiers(scene: BattleScene, player: boolean = true): v
if (!modifierTypes.hasOwnProperty(modifierName)) { if (!modifierTypes.hasOwnProperty(modifierName)) {
return; return;
} // if the modifier does not exist, we skip it } // if the modifier does not exist, we skip it
const modifierType: ModifierType = modifierTypes[modifierName](); let modifierType: ModifierType = modifierTypes[modifierName]();
const modifier: PersistentModifier = modifierType.withIdFromFunc(modifierTypes[modifierName]).newModifier() as PersistentModifier; 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; modifier.stackCount = qty;
if (player) { if (player) {
scene.addModifier(modifier, true, false, false, true); scene.addModifier(modifier, true, false, false, true);

View File

@ -137,7 +137,7 @@ export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null;
* - BerryType is for BERRY * - BerryType is for BERRY
* - SpeciesStatBoosterItem is for SPECIES_STAT_BOOSTER * - SpeciesStatBoosterItem is for SPECIES_STAT_BOOSTER
*/ */
interface ModifierOverride { export interface ModifierOverride {
name: keyof typeof modifierTypes & string, name: keyof typeof modifierTypes & string,
count?: integer count?: integer
type?: TempBattleStat|Stat|Nature|Type|BerryType|SpeciesStatBoosterItem 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 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. * 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())); loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter.introVisuals.initSprite()));
// Load Mystery Encounter Exclamation bubble and sfx // Load Mystery Encounter Exclamation bubble and sfx
loadEnemyAssets.push(new Promise<void>(resolve => { 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.loadAtlas("exclaim", "mystery-encounters");
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve());
if (!this.scene.load.isLoading()) { if (!this.scene.load.isLoading()) {

View File

@ -1,6 +1,6 @@
import { Button } from "#app/enums/buttons"; import { Button } from "#app/enums/buttons";
import { CommandPhase, MessagePhase, VictoryPhase } from "#app/phases"; 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 MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import GameManager from "../utils/gameManager"; import GameManager from "../utils/gameManager";
@ -26,7 +26,18 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>(); const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
uiHandler.processInput(Button.ACTION); 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 // If a battle is started, fast forward to end of the battle
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
@ -48,7 +59,6 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb
uiHandler.processInput(Button.ACTION); uiHandler.processInput(Button.ACTION);
}); });
if (isBattle) {
await game.phaseInterceptor.to(CommandPhase); await game.phaseInterceptor.to(CommandPhase);
} else { } else {
await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); await game.phaseInterceptor.to(MysteryEncounterRewardsPhase);
@ -60,7 +70,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN
game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>(); const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
uiHandler.processInput(Button.ACTION); uiHandler.processInput(Button.ACTION);
}); }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase));
if (game.isCurrentPhase(MessagePhase)) { if (game.isCurrentPhase(MessagePhase)) {
await game.phaseInterceptor.run(MessagePhase); await game.phaseInterceptor.run(MessagePhase);
@ -70,7 +80,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN
game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>(); const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
uiHandler.processInput(Button.ACTION); uiHandler.processInput(Button.ACTION);
}); }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase));
await game.phaseInterceptor.to(MysteryEncounterPhase, true); 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 { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter";
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; 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 { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
@ -131,6 +131,28 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(itemModifier.stackCount).toBe(1); 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 () => { it("should be disabled if player does not have enough money", async () => {
scene.money = 0; scene.money = 0;
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
@ -221,6 +243,64 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(healingCharmAfter.stackCount).toBe(1); 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 () => { it("should be disabled if player does not have any proper items", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
@ -325,6 +405,35 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(berryPouchAfter.stackCount).toBe(1); 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 () => { it("should be disabled if player does not have any proper items", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); 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 * as GameMode from "#app/game-mode";
import { GameModes, getGameMode } from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { ModifierOverride } from "#app/overrides";
/** /**
* Helper to handle overrides in tests * Helper to handle overrides in tests
@ -117,6 +118,12 @@ export class OverridesHelper {
return spy; 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[]) { private log(...params: any[]) {
console.log("Overrides:", ...params); console.log("Overrides:", ...params);
} }

View File

@ -48,6 +48,14 @@ import {
PostMysteryEncounterPhase PostMysteryEncounterPhase
} from "#app/phases/mystery-encounter-phases"; } from "#app/phases/mystery-encounter-phases";
export interface PromptHandler {
phaseTarget?;
mode?;
callback?;
expireFn?;
awaitingActionInput?;
}
export default class PhaseInterceptor { export default class PhaseInterceptor {
public scene; public scene;
public phases = {}; public phases = {};
@ -56,7 +64,7 @@ export default class PhaseInterceptor {
private interval; private interval;
private promptInterval; private promptInterval;
private intervalRun; private intervalRun;
private prompts; private prompts: PromptHandler[];
private phaseFrom; private phaseFrom;
private inProgress; private inProgress;
private originalSetMode; private originalSetMode;
@ -337,6 +345,7 @@ export default class PhaseInterceptor {
* @param mode - The mode of the UI. * @param mode - The mode of the UI.
* @param callback - The callback function to execute. * @param callback - The callback function to execute.
* @param expireFn - The function to determine if the prompt has expired. * @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) { addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void, awaitingActionInput: boolean = false) {
this.prompts.push({ this.prompts.push({