From b31a23e4017c0c171003e16deb13a00d32564e9b Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Fri, 26 Jul 2024 08:43:25 -0400 Subject: [PATCH 1/5] add absolute avarice encounter --- src/battle-scene.ts | 12 +- .../encounters/absolute-avarice-encounter.ts | 394 ++++++++++++++++++ .../encounters/fight-or-flight-encounter.ts | 2 +- .../mystery-encounter-requirements.ts | 35 +- .../mystery-encounters/mystery-encounter.ts | 2 +- .../mystery-encounters/mystery-encounters.ts | 3 +- .../utils/encounter-phase-utils.ts | 2 +- src/field/mystery-encounter-intro.ts | 18 +- src/locales/en/mystery-encounter.ts | 2 + .../absolute-avarice-dialogue.ts | 27 ++ .../fiery-fallout-dialogue.ts | 6 +- .../lost-at-sea-dialogue.ts | 6 +- .../mysterious-challengers-dialogue.ts | 6 +- .../mysterious-chest-dialogue.ts | 4 +- .../safari-zone-dialogue.ts | 4 +- .../slumbering-snorlax-dialogue.ts | 6 +- .../the-strong-stuff-dialogue.ts | 2 +- src/phases.ts | 2 +- 18 files changed, 492 insertions(+), 41 deletions(-) create mode 100644 src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts create mode 100644 src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index a031ee0bca2..c9409d7cab0 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2684,18 +2684,22 @@ export default class BattleScene extends SceneBase { while (availableEncounters.length === 0 && tier >= 0) { availableEncounters = biomeMysteryEncounters .filter((encounterType) => { - if (allMysteryEncounters[encounterType].encounterTier !== tier) { // Encounter is in tier + const encounterCandidate = allMysteryEncounters[encounterType]; + if (!encounterCandidate) { return false; } - if (!allMysteryEncounters[encounterType]?.meetsRequirements(this)) { // Meets encounter requirements + if (encounterCandidate.encounterTier !== tier) { // Encounter is in tier + return false; + } + if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements return false; } if (!isNullOrUndefined(previousEncounter) && encounterType === previousEncounter) { // Previous encounter was not this one return false; } if (this.mysteryEncounterData.encounteredEvents?.length > 0 && // Encounter has not exceeded max allowed encounters - allMysteryEncounters[encounterType].maxAllowedEncounters > 0 - && this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === encounterType).length >= allMysteryEncounters[encounterType].maxAllowedEncounters) { + encounterCandidate.maxAllowedEncounters > 0 + && this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === encounterType).length >= encounterCandidate.maxAllowedEncounters) { return false; } return true; diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts new file mode 100644 index 00000000000..f64d387a7fe --- /dev/null +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -0,0 +1,394 @@ +import { EnemyPartyConfig, generateModifierTypeOption, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon from "#app/field/pokemon"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { MoneyRequirement, 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 { ModifierRewardPhase, 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"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:absoluteAvarice"; + +/** + * Delibird-y 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: "petaya_berry", + fileRoot: "items", + isItem: true, + x: 20, + y: -17, + 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: "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 + }, + ]) + .withOnVisualsStart((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const greedentSprites = encounter.introVisuals.getSpriteAtIndex(0); + + scene.playSound("Follow Me"); + + // scene.tweens.add({ + // targets: greedentSprites, + // duration: 600, + // ease: "Cubic.easeOut", + // yoyo: true, + // y: "+=50", + // x: "-=60", + // scale: 1.2, + // onComplete: () => { + // // Bounce the Greedent + // scene.tweens.add({ + // targets: greedentSprites, + // duration: 300, + // ease: "Cubic.easeOut", + // yoyo: true, + // y: "-=20", + // loop: 1, + // }); + // } + // }); + + // Slide left + scene.tweens.add({ + targets: greedentSprites, + duration: 500, + ease: "Cubic.easeOut", + x: "-=300", + onComplete: () => { + // Slide back right, lower + greedentSprites[0].y += 80; + greedentSprites[1].y += 80; + scene.tweens.add({ + targets: greedentSprites, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + x: "+=140", + onComplete: () => { + // Slide back right, higher + greedentSprites[0].y -= 80; + greedentSprites[1].y -= 80; + scene.tweens.add({ + targets: greedentSprites, + duration: 500, + ease: "Cubic.easeOut", + x: "+=300", + onComplete: () => { + // Bounce the Greedent + scene.tweens.add({ + targets: greedentSprites, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=20", + loop: 1, + }); + } + }); + } + }); + } + }); + + const berryAddDelay = 200; + + const animationOrder = ["starf", "sitrus", "lansat", "salac", "apicot", "enigma", "liechi", "ganlon", "lum", "petaya", "leppa"]; + + animationOrder.forEach((berry, i) => { + const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey.includes(berry)); + const [ sprite, tintSprite ] = encounter.introVisuals.getSpriteAtIndex(introVisualsIndex); + // const [ sprite, tintSprite ] = [berrySprites[i * 2], berrySprites[i * 2 + 1]]; + scene.time.delayedCall(berryAddDelay * i + 300, () => { + if (sprite) { + sprite.setVisible(true); + } + if (tintSprite) { + tintSprite.setVisible(true); + } + }); + }); + + return true; + }) + .withIntroDialogue([ + { + text: `${namespace}:intro`, + } + ]) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) + .withOutroDialogue([ + { + text: `${namespace}:outro`, + } + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + + scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); + // scene.loadSe("Follow Me", "battle_anims"); + + // 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(); + 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: 5, + // nature: Nature.BOLD, + 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:2:stat_boost`); + 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`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney, true, false); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player an Ability Charm + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + secondOptionPrompt: `${namespace}:option:2:select_prompt`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const modifier = encounter.misc.chosenModifier; + // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed + if (modifier.type.name.includes("Berry")) { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + } + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + secondOptionPrompt: `${namespace}:option:3:select_prompt`, + selected: [ + { + text: `${namespace}:option:3:selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const modifier = encounter.misc.chosenModifier; + // Give the player a Berry Pouch + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index f4460de47d7..77ef97ace84 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -160,7 +160,7 @@ export const FightOrFlightEncounter: IMysteryEncounter = config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(pokemon)); - queueEncounterMessage(pokemon.scene, `${namespace}:boss_enraged`); + queueEncounterMessage(pokemon.scene, `${namespace}option:2:boss_enraged`); pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1)); }; await showEncounterText(scene, `${namespace}:option:2:bad_result`); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index b2b0cee5f38..1596cb6b549 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,5 +1,4 @@ import { PlayerPokemon } from "#app/field/pokemon"; -import { ModifierType } from "#app/modifier/modifier-type"; import BattleScene from "#app/battle-scene"; import { isNullOrUndefined } from "#app/utils"; import { Abilities } from "#enums/abilities"; @@ -235,28 +234,36 @@ export class PartySizeRequirement extends EncounterSceneRequirement { } export class PersistentModifierRequirement extends EncounterSceneRequirement { - requiredItems?: ModifierType[]; // TODO: not implemented - constructor(item: ModifierType | ModifierType[]) { + requiredHeldItemModifiers: string[]; + minNumberOfItems: number; + + constructor(heldItem: string | string[], minNumberOfItems: number = 1) { super(); - this.requiredItems = Array.isArray(item) ? item : [item]; + this.minNumberOfItems = minNumberOfItems; + this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; } meetsRequirement(scene: BattleScene): boolean { - const items = scene.modifiers; - - if (!isNullOrUndefined(items) && this?.requiredItems.length > 0 && this.requiredItems.filter((searchingMod) => - items.filter((itemInScene) => itemInScene.type.id === searchingMod.id).length > 0).length === 0) { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) { return false; } - return true; + let modifierCount = 0; + this.requiredHeldItemModifiers.forEach(modifier => { + const matchingMods = scene.findModifiers(m => m.constructor.name === modifier); + if (matchingMods?.length > 0) { + matchingMods.forEach(matchingMod => { + modifierCount += matchingMod.stackCount; + }); + } + }); + + return modifierCount >= this.minNumberOfItems; } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - const requiredItemsInInventory = this.requiredItems.filter((a) => { - scene.modifiers.filter((itemInScene) => itemInScene.type.id === a.id).length > 0; - }); - if (requiredItemsInInventory.length > 0) { - return ["requiredItem", requiredItemsInInventory[0].name]; + if (this.requiredHeldItemModifiers.length > 0) { + return ["requiredItem", this.requiredHeldItemModifiers[0]]; } return null; } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 895d6bd2da0..51815e3ccb7 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -159,7 +159,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { if (!isNullOrUndefined(encounter)) { Object.assign(this, encounter); } - this.encounterTier = this.encounterTier ? this.encounterTier : MysteryEncounterTier.COMMON; + this.encounterTier = !isNullOrUndefined(this.encounterTier) ? this.encounterTier : MysteryEncounterTier.COMMON; this.dialogue = this.dialogue ?? {}; // Default max is 1 for ROGUE encounters, 3 for others this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3; diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 1f7ad880fa1..c41fd8dfe53 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -17,6 +17,7 @@ import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-encounter"; import { OfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter"; import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; +import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -238,7 +239,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter; allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter; allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter; - // allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = Abs; + allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = AbsoluteAvariceEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 0064d6a105f..52b67cb4a47 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -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 { diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 59cdb90fc30..467d44c23cd 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -149,7 +149,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } } - if (alpha) { + if (!isNaN(alpha)) { sprite.setAlpha(alpha); tintSprite.setAlpha(alpha); } @@ -289,6 +289,22 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con }); } + /** + * Returns a Sprite/TintSprite pair + * @param index + */ + getSpriteAtIndex(index: number): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + ret.push(this.getAt(index * 2)); // Sprite + ret.push(this.getAt(index * 2 + 1)); // Tint Sprite + + return ret; + } + getSprites(): Phaser.GameObjects.Sprite[] { if (!this.spriteConfigs) { return; diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 59931894e6a..7cb6cb98178 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -14,6 +14,7 @@ import { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-s import { pokemonSalesmanDialogue } from "#app/locales/en/mystery-encounters/pokemon-salesman-dialogue"; import { offerYouCantRefuseDialogue } from "#app/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue"; import { delibirdyDialogue } from "#app/locales/en/mystery-encounters/delibirdy-dialogue"; +import { absoluteAvariceDialogue } from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue"; /** * Patterns that can be used: @@ -53,4 +54,5 @@ export const mysteryEncounter = { pokemonSalesman: pokemonSalesmanDialogue, offerYouCantRefuse: offerYouCantRefuseDialogue, delibirdy: delibirdyDialogue, + absoluteAvarice: absoluteAvariceDialogue, } as const; diff --git a/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts new file mode 100644 index 00000000000..69796a88d86 --- /dev/null +++ b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts @@ -0,0 +1,27 @@ +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: "You'll show this Greedent what\nhappens to those who steal from you!", + }, + 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 stash of berries in a flash! + $Patting its stomach, it looks at you appreciatively. + $Perhaps you could feed it more berries on your adventure... + $The Greedent wants to join your party!`, + }, + } +}; diff --git a/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts index 2fbf5a15bda..c8c5813b25e 100644 --- a/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts +++ b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts @@ -5,20 +5,20 @@ export const fieryFalloutDialogue = { query: "What will you do?", option: { 1: { - label: "Find the source", + label: "Find the Source", tooltip: "(?) Discover the source\n(-) Hard Battle", selected: `You push through the storm, and find two Volcarona in the middle of a mating dance! $They don't take kindly to the interruption and attack!` }, 2: { - label: "Hunker down", + label: "Hunker Down", tooltip: "(-) Suffer the effects of the weather", selected: `The weather effects cause significant\nharm as you struggle to find shelter! $Your party takes 20% Max HP damage!`, target_burned: "Your {{burnedPokemon}} also became burned!" }, 3: { - label: "Your Fire types help", + label: "Your Fire Types Help", tooltip: "(+) End the conditions\n(+) Gain a Charcoal", disabled_tooltip: "You need at least 2 Fire Type Pokémon to choose this", selected: `Your {{option3PrimaryName}} and {{option3SecondaryName}} guide you to where two Volcarona are in the middle of a mating dance! diff --git a/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts index cb1de1315cd..16b7661e174 100644 --- a/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts +++ b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts @@ -5,7 +5,7 @@ export const lostAtSeaDialogue = { query: "What will you do?", option: { 1: { - label: "{{option1PrimaryName}} can help", + label: "{{option1PrimaryName}} Might Help", label_disabled: "Can't {{option1RequiredMove}}", tooltip: "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP", tooltip_disabled: "You have no Pokémon to {{option1RequiredMove}} on", @@ -13,7 +13,7 @@ export const lostAtSeaDialogue = { \${{option1PrimaryName}} seems to also have gotten stronger in this time of need!`, }, 2: { - label: "{{option2PrimaryName}} can help", + label: "{{option2PrimaryName}} Might Help", label_disabled: "Can't {{option2RequiredMove}}", tooltip: "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP", tooltip_disabled: "You have no Pokémon to {{option2RequiredMove}} with", @@ -21,7 +21,7 @@ export const lostAtSeaDialogue = { \${{option2PrimaryName}} seems to also have gotten stronger in this time of need!`, }, 3: { - label: "Wander aimlessly", + label: "Wander Aimlessly", tooltip: "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", selected: `You float about in the boat, steering without direction until you finally spot a landmark you remember. $You and your Pokémon are fatigued from the whole ordeal.`, diff --git a/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts index 56c78e7e5f8..8586a524fb1 100644 --- a/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts +++ b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts @@ -5,15 +5,15 @@ export const mysteriousChallengersDialogue = { query: "Who will you battle?", option: { 1: { - label: "A clever, mindful foe", + label: "A Clever, Mindful Foe", tooltip: "(-) Standard Battle\n(+) Move Item Rewards", }, 2: { - label: "A strong foe", + label: "A Strong Foe", tooltip: "(-) Hard Battle\n(+) Good Rewards", }, 3: { - label: "The mightiest foe", + label: "The Mightiest Foe", tooltip: "(-) Brutal Battle\n(+) Great Rewards", }, selected: "The trainer steps forward...", diff --git a/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts index 74a578cfe39..f351851ebaf 100644 --- a/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts +++ b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts @@ -5,7 +5,7 @@ export const mysteriousChestDialogue = { query: "Will you open it?", option: { 1: { - label: "Open it", + label: "Open It", tooltip: "@[SUMMARY_BLUE]{(35%) Something terrible}\n@[SUMMARY_GREEN]{(40%) Okay Rewards}\n@[SUMMARY_GREEN]{(20%) Good Rewards}\n@[SUMMARY_GREEN]{(4%) Great Rewards}\n@[SUMMARY_GREEN]{(1%) Amazing Rewards}", selected: "You open the chest to find...", normal: "Just some normal tools and items.", @@ -16,7 +16,7 @@ export const mysteriousChestDialogue = { $Your {{pokeName}} jumps in front of you\nbut is KOed in the process.`, }, 2: { - label: "It's too risky, leave", + label: "Too Risky, Leave", tooltip: "(-) No Rewards", selected: "You hurry along your way,\nwith a slight feeling of regret.", }, diff --git a/src/locales/en/mystery-encounters/safari-zone-dialogue.ts b/src/locales/en/mystery-encounters/safari-zone-dialogue.ts index ae77aa447b8..20d03567b83 100644 --- a/src/locales/en/mystery-encounters/safari-zone-dialogue.ts +++ b/src/locales/en/mystery-encounters/safari-zone-dialogue.ts @@ -22,12 +22,12 @@ export const safariZoneDialogue = { selected: "You throw a Pokéball!", }, 2: { - label: "Throw bait", + label: "Throw Bait", tooltip: "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", selected: "You throw some bait!", }, 3: { - label: "Throw mud", + label: "Throw Mud", tooltip: "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", selected: "You throw some mud!", }, diff --git a/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts index 92244573c9b..75e4a4adfb6 100644 --- a/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts +++ b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts @@ -6,19 +6,19 @@ export const slumberingSnorlaxDialogue = { query: "What will you do?", option: { 1: { - label: "Battle it", + label: "Battle It", tooltip: "(-) Fight Sleeping Snorlax\n(+) Special Reward", selected: "You approach the\nPokémon without fear.", }, 2: { - label: "Wait for it to move", + label: "Wait for It to Move", tooltip: "(-) Wait a Long Time\n(+) Recover Party", selected: `.@d{32}.@d{32}.@d{32} $You wait for a time, but the Snorlax's yawns make your party sleepy...`, rest_result: "When you all awaken, the Snorlax is no where to be found -\nbut your Pokémon are all healed!", }, 3: { - label: "Steal its item", + label: "Steal Its Item", tooltip: "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward", disabled_tooltip: "Your Pokémon need to know certain moves to choose this", selected: `Your {{option3PrimaryName}} uses {{option3PrimaryMove}}! diff --git a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts index c65019b539f..c1a35c4f68a 100644 --- a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts @@ -5,7 +5,7 @@ export const theStrongStuffDialogue = { query: "What will you do?", option: { 1: { - label: "Let it touch you", + label: "Let It Touch You", tooltip: "(?) Something awful or amazing might happen", selected: "You black out.", selected_2: `@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained. diff --git a/src/phases.ts b/src/phases.ts index 982cc04031e..6c18a3439f5 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -903,7 +903,7 @@ export class EncounterPhase extends BattlePhase { loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter.introVisuals.initSprite())); // Load Mystery Encounter Exclamation bubble and sfx loadEnemyAssets.push(new Promise(resolve => { - this.scene.loadSe("GEN8- Exclaim.wav", "battle_anims", "GEN8- Exclaim.wav"); + this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); this.scene.loadAtlas("exclaim", "mystery-encounters"); this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); if (!this.scene.load.isLoading()) { From 7b2f21dc8c4efb6018c8c6775b1bfc640750c89a Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Fri, 26 Jul 2024 23:02:15 -0400 Subject: [PATCH 2/5] finish absolute avarice encounter --- .../encounters/absolute-avarice-encounter.ts | 388 ++++++++++++------ .../encounters/fiery-fallout-encounter.ts | 9 +- .../encounters/pokemon-salesman-encounter.ts | 2 +- .../shady-vitamin-dealer-encounter.ts | 10 +- .../mystery-encounters/mystery-encounter.ts | 2 +- .../utils/encounter-pokemon-utils.ts | 44 +- .../absolute-avarice-dialogue.ts | 13 +- src/overrides.ts | 4 +- 8 files changed, 312 insertions(+), 160 deletions(-) diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index f64d387a7fe..a63fc527324 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -1,21 +1,26 @@ -import { EnemyPartyConfig, generateModifierTypeOption, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import Pokemon from "#app/field/pokemon"; -import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +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 { MoneyRequirement, PersistentModifierRequirement } from "../mystery-encounter-requirements"; +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 { ModifierRewardPhase, StatChangePhase } from "#app/phases"; +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"; @@ -92,15 +97,6 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = hidden: true, disableAnimation: true }, - { - spriteKey: "petaya_berry", - fileRoot: "items", - isItem: true, - x: 20, - y: -17, - hidden: true, - disableAnimation: true - }, { spriteKey: "enigma_berry", fileRoot: "items", @@ -119,6 +115,15 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = hidden: true, disableAnimation: true }, + { + spriteKey: "petaya_berry", + fileRoot: "items", + isItem: true, + x: 30, + y: -17, + hidden: true, + disableAnimation: true + }, { spriteKey: "ganlon_berry", fileRoot: "items", @@ -136,7 +141,8 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = y: -2, hidden: true, disableAnimation: true - }, { + }, + { spriteKey: "starf_berry", fileRoot: "items", isItem: true, @@ -146,92 +152,11 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = disableAnimation: true }, ]) + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) .withOnVisualsStart((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - const greedentSprites = encounter.introVisuals.getSpriteAtIndex(0); - - scene.playSound("Follow Me"); - - // scene.tweens.add({ - // targets: greedentSprites, - // duration: 600, - // ease: "Cubic.easeOut", - // yoyo: true, - // y: "+=50", - // x: "-=60", - // scale: 1.2, - // onComplete: () => { - // // Bounce the Greedent - // scene.tweens.add({ - // targets: greedentSprites, - // duration: 300, - // ease: "Cubic.easeOut", - // yoyo: true, - // y: "-=20", - // loop: 1, - // }); - // } - // }); - - // Slide left - scene.tweens.add({ - targets: greedentSprites, - duration: 500, - ease: "Cubic.easeOut", - x: "-=300", - onComplete: () => { - // Slide back right, lower - greedentSprites[0].y += 80; - greedentSprites[1].y += 80; - scene.tweens.add({ - targets: greedentSprites, - duration: 300, - ease: "Cubic.easeOut", - yoyo: true, - x: "+=140", - onComplete: () => { - // Slide back right, higher - greedentSprites[0].y -= 80; - greedentSprites[1].y -= 80; - scene.tweens.add({ - targets: greedentSprites, - duration: 500, - ease: "Cubic.easeOut", - x: "+=300", - onComplete: () => { - // Bounce the Greedent - scene.tweens.add({ - targets: greedentSprites, - duration: 300, - ease: "Cubic.easeOut", - yoyo: true, - y: "-=20", - loop: 1, - }); - } - }); - } - }); - } - }); - - const berryAddDelay = 200; - - const animationOrder = ["starf", "sitrus", "lansat", "salac", "apicot", "enigma", "liechi", "ganlon", "lum", "petaya", "leppa"]; - - animationOrder.forEach((berry, i) => { - const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey.includes(berry)); - const [ sprite, tintSprite ] = encounter.introVisuals.getSpriteAtIndex(introVisualsIndex); - // const [ sprite, tintSprite ] = [berrySprites[i * 2], berrySprites[i * 2 + 1]]; - scene.time.delayedCall(berryAddDelay * i + 300, () => { - if (sprite) { - sprite.setVisible(true); - } - if (tintSprite) { - tintSprite.setVisible(true); - } - }); - }); + doGreedentSpriteSteal(scene); + doBerrySpritePile(scene); return true; }) @@ -251,8 +176,8 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; + scene.loadSe("PRSFX- Bug Bite", "battle_anims"); scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); - // scene.loadSe("Follow Me", "battle_anims"); // Get all player berry items, remove from party, and store reference const berryItems = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; @@ -288,13 +213,13 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = { species: getPokemonSpecies(Species.GREEDENT), isBoss: true, - bossSegments: 5, + bossSegments: 3, // nature: Nature.BOLD, 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:2:stat_boost`); + 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)); } } @@ -317,15 +242,32 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = }, ], }) - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney, true, false); - return true; - }) .withOptionPhase(async (scene: BattleScene) => { - // Give the player an Ability Charm - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); - leaveEncounterWithoutBattle(scene, true); + // 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() ) @@ -335,7 +277,6 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = .withDialogue({ buttonLabel: `${namespace}:option:2:label`, buttonTooltip: `${namespace}:option:2:tooltip`, - secondOptionPrompt: `${namespace}:option:2:select_prompt`, selected: [ { text: `${namespace}:option:2:selected`, @@ -344,19 +285,27 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; - const modifier = encounter.misc.chosenModifier; - // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed - if (modifier.type.name.includes("Berry")) { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); - } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); - } + const berryMap = encounter.misc.berryItemsMap; - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + // 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); }) @@ -368,27 +317,196 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = .withDialogue({ buttonLabel: `${namespace}:option:3:label`, buttonTooltip: `${namespace}:option:3:tooltip`, - secondOptionPrompt: `${namespace}:option:3:select_prompt`, 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) => { - const encounter = scene.currentBattle.mysteryEncounter; - const modifier = encounter.misc.chosenModifier; - // Give the player a Berry Pouch - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); - - // Remove the modifier if its stacks go to 0 - modifier.stackCount -= 1; - if (modifier.stackCount === 0) { - scene.removeModifier(modifier); - } + // Let it have the food + // Greedent joins the team, level equal to 2 below highest party member + const level = getHighestLevelPlayerPokemon(scene).level; + 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); + + // scene.playSound("Follow Me"); + 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(); +} diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 2da9ade0ff0..c7933129ff9 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -1,6 +1,6 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { modifierTypes, } from "#app/modifier/modifier-type"; +import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; @@ -17,7 +17,7 @@ import { WeatherType } from "#app/data/weather"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { StatusEffect } from "#app/data/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -246,9 +246,8 @@ function giveLeadPokemonCharcoal(scene: BattleScene) { // Give first party pokemon Charcoal for free at end of battle const leadPokemon = scene.getParty()?.[0]; if (leadPokemon) { - const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]); - scene.addModifier(charcoal.type.newModifier(leadPokemon), true); - scene.updateModifiers(); + const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]).type as AttackTypeBoosterModifierType; + applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name); queueEncounterMessage(scene, `${namespace}:found_charcoal`); } diff --git a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts index b5625cd349d..d1750119add 100644 --- a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts @@ -133,7 +133,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = // "Catch" purchased pokemon const data = new PokemonData(purchasedPokemon); data.player = false; - await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true); + await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true, true); leaveEncounterWithoutBattle(scene, true); }) diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 19248d7a5bd..bd7afdaa832 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -10,7 +10,7 @@ import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoneyRequirement } from "../mystery-encounter-requirements"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -110,10 +110,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = const modifiers = encounter.misc.modifiers; for (const modType of modifiers) { - const modifier = modType.newModifier(chosenPokemon); - await scene.addModifier(modifier, true, false, false, true); + await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); } - scene.updateModifiers(true); leaveEncounterWithoutBattle(scene); }) @@ -195,10 +193,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = const modifiers = encounter.misc.modifiers; for (const modType of modifiers) { - const modifier = modType.newModifier(chosenPokemon); - await scene.addModifier(modifier, true, false, false, true); + await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); } - scene.updateModifiers(true); leaveEncounterWithoutBattle(scene); }) diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 51815e3ccb7..502d84d9124 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -167,13 +167,13 @@ export default class IMysteryEncounter implements IMysteryEncounter { this.requirements = this.requirements ? this.requirements : []; this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false; this.autoHideIntroVisuals = !isNullOrUndefined(this.autoHideIntroVisuals) ? this.autoHideIntroVisuals : true; - this.startOfBattleEffects = this.startOfBattleEffects ?? []; // Reset any dirty flags or encounter data this.startOfBattleEffectsComplete = false; this.lockEncounterRewardTiers = true; this.dialogueTokens = {}; this.enemyPartyConfigs = []; + this.startOfBattleEffects = []; this.introVisuals = null; this.misc = null; this.expMultiplier = 1; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index b33607c6443..a11e65c5c54 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -17,7 +17,7 @@ import { Type } from "#app/data/type"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; export interface MysteryEncounterPokemonData { spriteScale?: number @@ -233,6 +233,36 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb pokemon.calculateStats(); } +/** + * Will attempt to add a new modifier to a Pokemon. + * If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified. + * @param scene + * @param pokemon + * @param modType + * @param fallbackModifierType + */ +export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon, modType: PokemonHeldItemModifierType, fallbackModifierType?: PokemonHeldItemModifierType) { + // Check if the Pokemon has max stacks of that item already + const existing = scene.findModifier(m => ( + m instanceof PokemonHeldItemModifier && + m.type.id === modType.id && + m.pokemonId === pokemon.id + )) as PokemonHeldItemModifier; + + // At max stacks + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + if (!fallbackModifierType) { + return; + } + + // Apply fallback + return applyModifierTypeToPlayerPokemon(scene, pokemon, fallbackModifierType); + } + + const modifier = modType.newModifier(pokemon); + await scene.addModifier(modifier, false, false, false, true); +} + /** * Alternative to using AttemptCapturePhase * Assumes player sprite is visible on the screen (this is intended for non-combat uses) @@ -407,7 +437,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, }); } -export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType, isObtain: boolean = false): Promise { +export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType, showCatchObtainMessage: boolean = true, isObtain: boolean = false): Promise { scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); @@ -433,7 +463,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); return new Promise(resolve => { - scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { + const doPokemonCatchMenu = () => { const end = () => { scene.pokemonInfoContainer.hide(); removePb(scene, pokeball); @@ -488,7 +518,13 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po addToParty(); } }); - }, 0, true); + }; + + if (showCatchObtainMessage) { + scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, doPokemonCatchMenu, 0, true); + } else { + doPokemonCatchMenu(); + } }); } diff --git a/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts index 69796a88d86..5d0935282f6 100644 --- a/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts +++ b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.ts @@ -7,7 +7,10 @@ export const absoluteAvariceDialogue = { 1: { label: "Battle It", tooltip: "(-) Tough Battle\n(+) Rewards from its Berry Hoard", - selected: "You'll show this Greedent what\nhappens to those who steal from you!", + 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", @@ -18,10 +21,10 @@ export const absoluteAvariceDialogue = { 3: { label: "Let It Have the Food", tooltip: "(-) Lose All Berries\n(?) The Greedent Will Like You", - selected: `The Greedent devours the entire stash of berries in a flash! - $Patting its stomach, it looks at you appreciatively. - $Perhaps you could feed it more berries on your adventure... - $The Greedent wants to join your party!`, + 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!`, }, } }; diff --git a/src/overrides.ts b/src/overrides.ts index b530032364d..1d006630983 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.ABSOLUTE_AVARICE; /** * MODIFIER / ITEM OVERRIDES From 132f5ac343f049b759d496205c46763ee8d37035 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sun, 28 Jul 2024 11:56:16 -0400 Subject: [PATCH 3/5] add unit tests and enhancements for item overrides in tests --- src/battle-scene.ts | 4 +- .../encounters/absolute-avarice-encounter.ts | 7 - .../encounters/delibirdy-encounter.ts | 74 ++++- .../utils/encounter-phase-utils.ts | 91 ++++-- src/modifier/modifier-type.ts | 78 +++-- src/modifier/modifier.ts | 10 +- src/overrides.ts | 8 +- .../mystery-encounter/encounterTestUtils.ts | 58 ++-- .../absolute-avarice-encounter.test.ts | 266 ++++++++++++++++++ .../encounters/delibirdy-encounter.test.ts | 111 +++++++- src/test/utils/overridesHelper.ts | 7 + src/test/utils/phaseInterceptor.ts | 11 +- 12 files changed, 634 insertions(+), 91 deletions(-) create mode 100644 src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c9409d7cab0..169c024d329 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1072,12 +1072,14 @@ export default class BattleScene extends SceneBase { this.field.add(newTrainer); } - // TODO: remove this once spawn rates are finalized + // TODO: remove these once ME spawn rates are finalized // let testStartingWeight = 0; // while (testStartingWeight < 3) { // calculateMEAggregateStats(this, testStartingWeight); // testStartingWeight += 2; // } + // calculateRareSpawnAggregateStats(this, 14); + // Check for mystery encounter // Can only occur in place of a standard wild battle, waves 10-180 if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && newWaveIndex < 180 && newWaveIndex > 10) { diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index a63fc527324..9077f0f1ab8 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -168,11 +168,6 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) - .withOutroDialogue([ - { - text: `${namespace}:outro`, - } - ]) .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; @@ -423,8 +418,6 @@ function doGreedentSpriteSteal(scene: BattleScene) { function doGreedentEatBerries(scene: BattleScene) { const greedentSprites = scene.currentBattle.mysteryEncounter.introVisuals.getSpriteAtIndex(0); - - // scene.playSound("Follow Me"); let index = 1; scene.tweens.add({ targets: greedentSprites, diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index c58f9775a04..dcc37cda8fe 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -1,18 +1,20 @@ -import { leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; -import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; +import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { BerryModifier, PokemonBaseStatModifier, PokemonBaseStatTotalModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, TerastallizeModifier } from "#app/modifier/modifier"; +import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonBaseStatModifier, PokemonBaseStatTotalModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PreserveBerryModifier, TerastallizeModifier } from "#app/modifier/modifier"; import { ModifierRewardPhase } from "#app/phases"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import i18next from "#app/plugins/i18n"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:delibirdy"; @@ -39,6 +41,10 @@ export const DelibirdyEncounter: IMysteryEncounter = .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(10, 180) .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Must have enough money for it to spawn at the very least + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn + new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), + new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) + )) .withIntroSpriteConfigs([ { spriteKey: Species.DELIBIRD.toString(), @@ -82,7 +88,7 @@ export const DelibirdyEncounter: IMysteryEncounter = .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 2.75) + .withSceneMoneyRequirement(0, 2.75) // Must have money to spawn .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, @@ -99,7 +105,19 @@ export const DelibirdyEncounter: IMysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Give the player an Ability Charm - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + // Check if the player has max stacks of that item already + const existing = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + } + leaveEncounterWithoutBattle(scene, true); }) .build() @@ -159,11 +177,34 @@ export const DelibirdyEncounter: IMysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; const modifier = encounter.misc.chosenModifier; + // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed if (modifier.type.name.includes("Berry")) { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + // Check if the player has max stacks of that Candy Jar already + const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + } } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + // Check if the player has max stacks of that Healing Charm already + const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + } } // Remove the modifier if its stacks go to 0 @@ -231,8 +272,19 @@ export const DelibirdyEncounter: IMysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; const modifier = encounter.misc.chosenModifier; - // Give the player a Berry Pouch - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + + // Check if the player has max stacks of Berry Pouch already + const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + } // Remove the modifier if its stacks go to 0 modifier.stackCount -= 1; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 52b67cb4a47..a0fe413339d 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,11 +1,11 @@ import { BattlerIndex, BattleType } from "#app/battle"; -import { biomeLinks } from "#app/data/biomes"; +import { biomeLinks, BiomePoolTier } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; -import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import * as Overrides from "#app/overrides"; import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, MovePhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; @@ -344,20 +344,10 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); let result: ModifierType = modifierTypes[modifierId]?.(); - // Gets tier of item by checking player item pool - const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); - Object.keys(modifierPool).every(modifierTier => { - const modType = modifierPool[modifierTier].find(m => { - if (m.modifierType.id === modifierId) { - return m; - } - }); - if (modType) { - result = modType.modifierType; - return false; - } - return true; - }); + // Populates item id and tier (order matters) + result = result + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; return new ModifierTypeOption(result, 0); @@ -881,3 +871,72 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n console.log(stats); } + + +/** + * TODO: remove once encounter spawn rate is finalized + * Just a helper function to calculate aggregate stats for MEs in a Classic run + * @param scene + * @param luckValue - 0 to 14 + */ +export function calculateRareSpawnAggregateStats(scene: BattleScene, luckValue: number) { + const numRuns = 1000; + let run = 0; + + const calculateNumRareEncounters = (): any[] => { + const bossEncountersByRarity = [0, 0, 0, 0]; + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); + // There are 12 wild boss floors + for (let i = 0; i < 12; i++) { + // Roll boss tier + // luck influences encounter rarity + let luckModifier = 0; + if (!isNaN(luckValue)) { + luckModifier = luckValue * 0.5; + } + const tierValue = Utils.randSeedInt(64 - luckModifier); + const tier = tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; + + switch (tier) { + default: + case BiomePoolTier.BOSS: + ++bossEncountersByRarity[0]; + break; + case BiomePoolTier.BOSS_RARE: + ++bossEncountersByRarity[1]; + break; + case BiomePoolTier.BOSS_SUPER_RARE: + ++bossEncountersByRarity[2]; + break; + case BiomePoolTier.BOSS_ULTRA_RARE: + ++bossEncountersByRarity[3]; + break; + } + } + + return bossEncountersByRarity; + }; + + const encounterRuns: number[][] = []; + while (run < numRuns) { + scene.executeWithSeedOffset(() => { + const bossEncountersByRarity = calculateNumRareEncounters(); + encounterRuns.push(bossEncountersByRarity); + }, 1000 * run); + run++; + } + + const n = encounterRuns.length; + // const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); + // const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; + // const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const ultraRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; + + const stats = `Avg Commons: ${commonMean}\nAvg Rare: ${rareMean}\nAvg Super Rare: ${superRareMean}\nAvg Ultra Rare: ${ultraRareMean}\n`; + + console.log(stats); +} diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index ae5b1ba0ee8..9a8504f16d3 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -109,11 +109,35 @@ export class ModifierType { return null; } + /** + * Populates item id for ModifierType instance + * @param func + */ withIdFromFunc(func: ModifierTypeFunc): ModifierType { this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func); return this; } + /** + * Populates item tier for ModifierType instance + * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) + * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * @param poolType - Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from + */ + withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { + const modifierPool = getModifierPoolForType(poolType); + Object.values(modifierPool).every(weightedModifiers => { + weightedModifiers.every(m => { + if (m.modifierType.id === this.id) { + this.tier = m.modifierType.tier; + return false; // Early lookup return if tier found + } + }); + return !this.tier; // Early lookup return if tier found + }); + return this; + } + newModifier(...args: any[]): Modifier { return this.newModifierFunc(this, args); } @@ -1841,6 +1865,21 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc { return modifierTypes[id]; } +/** + * Generates modifier options for a SelectModifierPhase + * @param count - Determines the number of items to generate + * @param party - Party is required for generating proper modifier pools + * @param modifierTiers - (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. + * @param customModifierSettings - (Optional) If specified, can customize the item shop rewards further. + * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` - If specified, will override the first X items to be specific modifier options (these should be pre-genned). + * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` - If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). + * - `guaranteedModifierTiers?: ModifierTier[]` - If specified, will override the next X items to be the specified tier. These can upgrade with luck. + * - `fillRemaining?: boolean` - Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. + * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, + * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. + * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). + * - `rerollMultiplier?: number` - If specified, can adjust the amount of money required for a shop reroll. If set to 0, the shop will not allow rerolls at all. + */ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] { const options: ModifierTypeOption[] = []; const retryCount = Math.min(count * 5, 50); @@ -1849,32 +1888,21 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined)); }); } else { - // Guaranteed mods first - if (customModifierSettings?.guaranteedModifierTypeOptions?.length) { - customModifierSettings?.guaranteedModifierTypeOptions.forEach((option) => { - options.push(option); - }); + // Guaranteed mod options first + if (customModifierSettings?.guaranteedModifierTypeOptions?.length > 0) { + options.push(...customModifierSettings.guaranteedModifierTypeOptions); } - // Guaranteed mod funcs second - if (customModifierSettings?.guaranteedModifierTypeFuncs?.length) { + // Guaranteed mod functions second + if (customModifierSettings?.guaranteedModifierTypeFuncs?.length > 0) { customModifierSettings?.guaranteedModifierTypeFuncs.forEach((mod, i) => { const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod); let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); - // Gets tier of item by checking player item pool - Object.keys(modifierPool).every(modifierTier => { - const modType = modifierPool[modifierTier].find(m => { - if (m.modifierType.id === modifierId) { - return m; - } - }); - if (modType) { - guaranteedMod = modType.modifierType; - return false; - } - return true; - }); + // Populates item id and tier + guaranteedMod = guaranteedMod + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; const option = new ModifierTypeOption(modType, 0); @@ -1883,7 +1911,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo } // Guaranteed tiers third - if (customModifierSettings?.guaranteedModifierTiers?.length) { + if (customModifierSettings?.guaranteedModifierTiers?.length > 0) { customModifierSettings?.guaranteedModifierTiers.forEach((tier) => { options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier)); }); @@ -1900,8 +1928,12 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo // OVERRIDE IF NECESSARY if (Overrides.ITEM_REWARD_OVERRIDE?.length) { options.forEach((mod, i) => { - // @ts-ignore: keeps throwing don't use string as index error in typedoc run - const override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.(); + let override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.(); + // Populates item id and tier + override = override + .withIdFromFunc(modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]) + .withTierFromPool(); + mod.type = (override instanceof ModifierTypeGenerator ? override.generateType(party) : override) || mod.type; }); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 72aaf6cbb41..165a1227c4a 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -21,7 +21,7 @@ import { VoucherType } from "../system/voucher"; import { FormChangeItem, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { Nature } from "#app/data/nature"; import * as Overrides from "../overrides"; -import { ModifierType, modifierTypes } from "./modifier-type"; +import { ModifierType, ModifierTypeGenerator, modifierTypes } from "./modifier-type"; import { Command } from "#app/ui/command-ui-handler.js"; import { Species } from "#enums/species"; import i18next from "i18next"; @@ -2683,8 +2683,12 @@ export function overrideModifiers(scene: BattleScene, player: boolean = true): v if (!modifierTypes.hasOwnProperty(modifierName)) { return; } // if the modifier does not exist, we skip it - const modifierType: ModifierType = modifierTypes[modifierName](); - const modifier: PersistentModifier = modifierType.withIdFromFunc(modifierTypes[modifierName]).newModifier() as PersistentModifier; + let modifierType: ModifierType = modifierTypes[modifierName](); + if (modifierType instanceof ModifierTypeGenerator) { + modifierType = (modifierType as ModifierTypeGenerator).generateType(scene.getParty(), [item.type]); + } + modifierType = modifierType.withIdFromFunc(modifierTypes[modifierName]); + const modifier: PersistentModifier = modifierType.newModifier(scene.getParty()[0]) as PersistentModifier; modifier.stackCount = qty; if (player) { scene.addModifier(modifier, true, false, false, true); diff --git a/src/overrides.ts b/src/overrides.ts index 1d006630983..d4e4a65134d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.ABSOLUTE_AVARICE; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; /** * MODIFIER / ITEM OVERRIDES @@ -137,7 +137,7 @@ export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounter * - BerryType is for BERRY * - SpeciesStatBoosterItem is for SPECIES_STAT_BOOSTER */ -interface ModifierOverride { +export interface ModifierOverride { name: keyof typeof modifierTypes & string, count?: integer type?: TempBattleStat|Stat|Nature|Type|BerryType|SpeciesStatBoosterItem @@ -155,4 +155,4 @@ export const NEVER_CRIT_OVERRIDE: boolean = false; * If less items are listed than rolled, only some items will be replaced * If more items are listed than rolled, only the first X items will be shown, where X is the number of items rolled. */ -export const ITEM_REWARD_OVERRIDE: Array = []; +export const ITEM_REWARD_OVERRIDE: Array = []; diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts index aa0551e78e2..a3eb505c090 100644 --- a/src/test/mystery-encounter/encounterTestUtils.ts +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -1,6 +1,6 @@ import { Button } from "#app/enums/buttons"; import { CommandPhase, MessagePhase, VictoryPhase } from "#app/phases"; -import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; import { Mode } from "#app/ui/ui"; import GameManager from "../utils/gameManager"; @@ -26,29 +26,39 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); - }); - - // If a battle is started, fast forward to end of the battle - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.clearPhaseQueue(); - game.scene.clearPhaseQueueSplice(); - game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); - game.endPhase(); - }); - - // Handle end of battle trainer messages - game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - // Handle egg hatch dialogue - game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); + }, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase)); if (isBattle) { + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + game.onNextPrompt("CheckSwitchPhase", Mode.MESSAGE, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + // If a battle is started, fast forward to end of the battle + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); + game.endPhase(); + }); + + // Handle end of battle trainer messages + game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // Handle egg hatch dialogue + game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + await game.phaseInterceptor.to(CommandPhase); } else { await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); @@ -60,7 +70,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); - }); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); if (game.isCurrentPhase(MessagePhase)) { await game.phaseInterceptor.run(MessagePhase); @@ -70,7 +80,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); - }); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); await game.phaseInterceptor.to(MysteryEncounterPhase, true); diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts new file mode 100644 index 00000000000..0df5ec9403f --- /dev/null +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -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.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(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index ee6ce0f6705..9cee29b130d 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -11,7 +11,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; -import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } from "#app/modifier/modifier"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; @@ -131,6 +131,28 @@ describe("Delibird-y - Mystery Encounter", () => { expect(itemModifier.stackCount).toBe(1); }); + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const abilityCharm = generateModifierTypeOption(scene, modifierTypes.ABILITY_CHARM).type.newModifier() as HiddenAbilityRateBoosterModifier; + abilityCharm.stackCount = 4; + await scene.addModifier(abilityCharm, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 1); + + const abilityCharmAfter = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(abilityCharmAfter).toBeDefined(); + expect(abilityCharmAfter.stackCount).toBe(4); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + it("should be disabled if player does not have enough money", async () => { scene.money = 0; await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); @@ -221,6 +243,64 @@ describe("Delibird-y - Mystery Encounter", () => { expect(healingCharmAfter.stackCount).toBe(1); }); + it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 99 Candy Jars + scene.modifiers = []; + const candyJar = generateModifierTypeOption(scene, modifierTypes.CANDY_JAR).type.newModifier() as LevelIncrementBoosterModifier; + candyJar.stackCount = 99; + await scene.addModifier(candyJar, true, false, false, true); + const sitrus = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type; + + // Sitrus berries on party + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); + const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(sitrusAfter.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter.stackCount).toBe(99); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierTypeOption(scene, modifierTypes.HEALING_CHARM).type.newModifier() as HealingBoosterModifier; + healingCharm.stackCount = 5; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Reviver Seed on party lead + const revSeed = generateModifierTypeOption(scene, modifierTypes.REVIVER_SEED).type; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter.stackCount).toBe(5); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + it("should be disabled if player does not have any proper items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); @@ -325,6 +405,35 @@ describe("Delibird-y - Mystery Encounter", () => { expect(berryPouchAfter.stackCount).toBe(1); }); + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierTypeOption(scene, modifierTypes.BERRY_POUCH).type.newModifier() as PreserveBerryModifier; + healingCharm.stackCount = 3; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Soul Dew on party lead + const soulDew = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter.stackCount).toBe(3); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + it("should be disabled if player does not have any proper items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts index 400bad716a7..bd4744503e0 100644 --- a/src/test/utils/overridesHelper.ts +++ b/src/test/utils/overridesHelper.ts @@ -8,6 +8,7 @@ import * as overrides from "#app/overrides"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { ModifierOverride } from "#app/overrides"; /** * Helper to handle overrides in tests @@ -117,6 +118,12 @@ export class OverridesHelper { return spy; } + starterHeldItems(modifiers: ModifierOverride[]) { + const spy = vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); + this.log(`Starting modifiers set to ${modifiers.map(m => JSON.stringify(m)).join(", ")}!`); + return spy; + } + private log(...params: any[]) { console.log("Overrides:", ...params); } diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 4e1f2acf586..a98b462006a 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -48,6 +48,14 @@ import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +export interface PromptHandler { + phaseTarget?; + mode?; + callback?; + expireFn?; + awaitingActionInput?; +} + export default class PhaseInterceptor { public scene; public phases = {}; @@ -56,7 +64,7 @@ export default class PhaseInterceptor { private interval; private promptInterval; private intervalRun; - private prompts; + private prompts: PromptHandler[]; private phaseFrom; private inProgress; private originalSetMode; @@ -337,6 +345,7 @@ export default class PhaseInterceptor { * @param mode - The mode of the UI. * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. + * @param awaitingActionInput */ addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void, awaitingActionInput: boolean = false) { this.prompts.push({ From 3b36836608a691504f3c864ac4c6b20845e1fcc7 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sun, 28 Jul 2024 12:12:11 -0400 Subject: [PATCH 4/5] fix unit test --- .../encounters/absolute-avarice-encounter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index 0df5ec9403f..4aaf5f3d518 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -84,7 +84,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { game.override.startingBiome(Biome.VOLCANO); await game.runToMysteryEncounter(); - expect(game.scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); }); it("should not spawn if player does not have enough berries", async () => { From 6115fc9b840cf58ece3cef2397ff52d8fcd026eb Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sun, 28 Jul 2024 15:20:58 -0400 Subject: [PATCH 5/5] cleanup absolute avarice PR --- .../encounters/absolute-avarice-encounter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index 9077f0f1ab8..c764589ac42 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -26,7 +26,7 @@ import { PokeballType } from "#app/data/pokeball"; const namespace = "mysteryEncounter:absoluteAvarice"; /** - * Delibird-y encounter. + * Absolute Avarice encounter. * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/58 | GitHub Issue #58} * @see For biome requirements check {@linkcode mysteryEncountersByBiome} */ @@ -209,7 +209,6 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = species: getPokemonSpecies(Species.GREEDENT), isBoss: true, bossSegments: 3, - // nature: Nature.BOLD, moveSet: [Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF], modifierTypes: bossModifierTypes, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], @@ -327,7 +326,7 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = .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; + 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;