Merge pull request #4210 from ben-lear/mystery-encounters

PR feedback from NightKev
This commit is contained in:
ImperialSympathizer 2024-09-13 09:04:00 -04:00 committed by GitHub
commit 1870926f3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 233 additions and 185 deletions

View File

@ -904,10 +904,10 @@ export default class BattleScene extends SceneBase {
} }
/** /**
* Removes a PlayerPokemon from the party, and clears modifiers for that Pokemon's id * Removes a {@linkcode PlayerPokemon} from the party, and clears modifiers for that Pokemon's id
* Useful for MEs/Challenges that remove Pokemon from the player party temporarily or permanently * Useful for MEs/Challenges that remove Pokemon from the player party temporarily or permanently
* @param pokemon * @param pokemon
* @param destroy - Default true. If true, will destroy the Pokemon object after removing * @param destroy Default true. If true, will destroy the {@linkcode PlayerPokemon} after removing
*/ */
removePokemonFromPlayerParty(pokemon: PlayerPokemon, destroy: boolean = true) { removePokemonFromPlayerParty(pokemon: PlayerPokemon, destroy: boolean = true) {
if (!pokemon) { if (!pokemon) {
@ -2939,10 +2939,10 @@ export default class BattleScene extends SceneBase {
/** /**
* Updates Exp and level values for Player's party, adding new level up phases as required * Updates Exp and level values for Player's party, adding new level up phases as required
* @param expValue - raw value of exp to split among participants, OR the base multiplier to use with waveIndex * @param expValue raw value of exp to split among participants, OR the base multiplier to use with waveIndex
* @param pokemonDefeated - If true, will increment Macho Brace stacks and give the party Pokemon friendship increases * @param pokemonDefeated If true, will increment Macho Brace stacks and give the party Pokemon friendship increases
* @param useWaveIndexMultiplier - Default false. If true, will multiply expValue by a scaling waveIndex multiplier. Not needed if expValue is already scaled by level/wave * @param useWaveIndexMultiplier Default false. If true, will multiply expValue by a scaling waveIndex multiplier. Not needed if expValue is already scaled by level/wave
* @param pokemonParticipantIds - Participants. If none are defined, no exp will be given. To spread evenly among the party, should pass all ids of party members. * @param pokemonParticipantIds Participants. If none are defined, no exp will be given. To spread evenly among the party, should pass all ids of party members.
*/ */
applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void { applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void {
const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds;
@ -3040,7 +3040,7 @@ export default class BattleScene extends SceneBase {
/** /**
* Loads or generates a mystery encounter * Loads or generates a mystery encounter
* @param encounterType - used to load session encounter when restarting game, etc. * @param encounterType used to load session encounter when restarting game, etc.
* @returns * @returns
*/ */
getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter { getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter {
@ -3111,10 +3111,11 @@ export default class BattleScene extends SceneBase {
return false; return false;
} }
const disabledModes = encounterCandidate.disabledGameModes; const disabledModes = encounterCandidate.disabledGameModes;
if (disabledModes && disabledModes.length > 0 && disabledModes.includes(this.gameMode.modeId)) { // Encounter is enabled for game mode if (disabledModes && disabledModes.length > 0
&& disabledModes.includes(this.gameMode.modeId)) { // Encounter is enabled for game mode
return false; return false;
} }
if (!encounterCandidate.meetsRequirements!(this)) { // Meets encounter requirements if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements
return false; return false;
} }
if (previousEncounter !== null && encounterType === previousEncounter) { // Previous encounter was not this one if (previousEncounter !== null && encounterType === previousEncounter) { // Previous encounter was not this one
@ -3148,7 +3149,7 @@ export default class BattleScene extends SceneBase {
encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)]; encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)];
// New encounter object to not dirty flags // New encounter object to not dirty flags
encounter = new MysteryEncounter(encounter); encounter = new MysteryEncounter(encounter);
encounter.populateDialogueTokensFromRequirements!(this); encounter.populateDialogueTokensFromRequirements(this);
return encounter; return encounter;
} }
} }

View File

@ -535,7 +535,7 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
/** /**
* Fetches animation configs to be used in a Mystery Encounter * Fetches animation configs to be used in a Mystery Encounter
* @param scene * @param scene
* @param encounterAnim - one or more animations to fetch * @param encounterAnim one or more animations to fetch
*/ */
export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> { export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> {
const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim];
@ -608,7 +608,7 @@ export function loadCommonAnimAssets(scene: BattleScene, startLoad?: boolean): P
/** /**
* Loads encounter animation assets to scene * Loads encounter animation assets to scene
* MUST be called after [initEncounterAnims()](./battle-anims.ts) to load all required animations properly * MUST be called after {@linkcode initEncounterAnims()} to load all required animations properly
* @param scene * @param scene
* @param startLoad * @param startLoad
*/ */
@ -1079,7 +1079,6 @@ export abstract class BattleAnim {
[AnimFrameTarget.USER]: [], [AnimFrameTarget.USER]: [],
[AnimFrameTarget.TARGET]: [] [AnimFrameTarget.TARGET]: []
}; };
const spritePriorities: integer[] = [];
const cleanUpAndComplete = () => { const cleanUpAndComplete = () => {
for (const ms of Object.values(spriteCache).flat()) { for (const ms of Object.values(spriteCache).flat()) {
@ -1104,8 +1103,8 @@ export abstract class BattleAnim {
this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ];
this.dstLine = [ 150, 75, targetInitialX, targetInitialY ]; this.dstLine = [ 150, 75, targetInitialX, targetInitialY ];
let r = anim!.frames.length; let totalFrames = anim!.frames.length;
let f = 0; let frameCount = 0;
let existingFieldSprites = scene.field.getAll().slice(0); let existingFieldSprites = scene.field.getAll().slice(0);
@ -1114,11 +1113,9 @@ export abstract class BattleAnim {
repeat: anim!.frames.length, repeat: anim!.frames.length,
onRepeat: () => { onRepeat: () => {
existingFieldSprites = scene.field.getAll().slice(0); existingFieldSprites = scene.field.getAll().slice(0);
const spriteFrames = anim!.frames[f]; const spriteFrames = anim!.frames[frameCount];
const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[f], targetInitialX, targetInitialY); const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[frameCount], targetInitialX, targetInitialY);
const u = 0; let graphicFrameCount = 0;
const t = 0;
let g = 0;
for (const frame of spriteFrames) { for (const frame of spriteFrames) {
if (frame.target !== AnimFrameTarget.GRAPHIC) { if (frame.target !== AnimFrameTarget.GRAPHIC) {
console.log("Encounter animations do not support targets"); console.log("Encounter animations do not support targets");
@ -1126,16 +1123,14 @@ export abstract class BattleAnim {
} }
const sprites = spriteCache[AnimFrameTarget.GRAPHIC]; const sprites = spriteCache[AnimFrameTarget.GRAPHIC];
if (g === sprites.length) { if (graphicFrameCount === sprites.length) {
const newSprite: Phaser.GameObjects.Sprite = scene.addFieldSprite(0, 0, anim!.graphic, 1); const newSprite: Phaser.GameObjects.Sprite = scene.addFieldSprite(0, 0, anim!.graphic, 1);
sprites.push(newSprite); sprites.push(newSprite);
scene.field.add(newSprite); scene.field.add(newSprite);
spritePriorities.push(1);
} }
const graphicIndex = g++; const graphicIndex = graphicFrameCount++;
const moveSprite = sprites[graphicIndex]; const moveSprite = sprites[graphicIndex];
spritePriorities[graphicIndex] = frame.priority;
if (!isNullOrUndefined(frame.priority)) { if (!isNullOrUndefined(frame.priority)) {
const setSpritePriority = (priority: integer) => { const setSpritePriority = (priority: integer) => {
if (existingFieldSprites.length > priority) { if (existingFieldSprites.length > priority) {
@ -1162,40 +1157,37 @@ export abstract class BattleAnim {
moveSprite.setBlendMode(frame.blendType === AnimBlendType.NORMAL ? Phaser.BlendModes.NORMAL : frame.blendType === AnimBlendType.ADD ? Phaser.BlendModes.ADD : Phaser.BlendModes.DIFFERENCE); moveSprite.setBlendMode(frame.blendType === AnimBlendType.NORMAL ? Phaser.BlendModes.NORMAL : frame.blendType === AnimBlendType.ADD ? Phaser.BlendModes.ADD : Phaser.BlendModes.DIFFERENCE);
} }
} }
if (anim?.frameTimedEvents.get(f)) { if (anim?.frameTimedEvents.get(frameCount)) {
for (const event of anim.frameTimedEvents.get(f)!) { for (const event of anim.frameTimedEvents.get(frameCount)!) {
r = Math.max((anim.frames.length - f) + event.execute(scene, this, frameTimedEventPriority), r); totalFrames = Math.max((anim.frames.length - frameCount) + event.execute(scene, this, frameTimedEventPriority), totalFrames);
} }
} }
const targets = Utils.getEnumValues(AnimFrameTarget); const targets = Utils.getEnumValues(AnimFrameTarget);
for (const i of targets) { for (const i of targets) {
const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t; const count = graphicFrameCount;
if (count < spriteCache[i].length) { if (count < spriteCache[i].length) {
const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length); const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length);
for (const rs of spritesToRemove) { for (const sprite of spritesToRemove) {
if (!rs.getData("locked") as boolean) { if (!sprite.getData("locked") as boolean) {
const spriteCacheIndex = spriteCache[i].indexOf(rs); const spriteCacheIndex = spriteCache[i].indexOf(sprite);
spriteCache[i].splice(spriteCacheIndex, 1); spriteCache[i].splice(spriteCacheIndex, 1);
if (i === AnimFrameTarget.GRAPHIC) { sprite.destroy();
spritePriorities.splice(spriteCacheIndex, 1);
}
rs.destroy();
} }
} }
} }
} }
f++; frameCount++;
r--; totalFrames--;
}, },
onComplete: () => { onComplete: () => {
for (const ms of Object.values(spriteCache).flat()) { for (const sprite of Object.values(spriteCache).flat()) {
if (ms && !ms.getData("locked")) { if (sprite && !sprite.getData("locked")) {
ms.destroy(); sprite.destroy();
} }
} }
if (r) { if (totalFrames) {
scene.tweens.addCounter({ scene.tweens.addCounter({
duration: Utils.getFrameMs(r), duration: Utils.getFrameMs(totalFrames),
onComplete: () => cleanUpAndComplete() onComplete: () => cleanUpAndComplete()
}); });
} else { } else {
@ -1277,7 +1269,7 @@ export class EncounterBattleAnim extends BattleAnim {
public oppAnim: boolean; public oppAnim: boolean;
constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon, oppAnim?: boolean) { constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon, oppAnim?: boolean) {
super(user, target || user, true); super(user, target ?? user, true);
this.encounterAnim = encounterAnim; this.encounterAnim = encounterAnim;
this.oppAnim = oppAnim ?? false; this.oppAnim = oppAnim ?? false;

View File

@ -2212,12 +2212,14 @@ export class TarShotTag extends BattlerTag {
} }
/** /**
* Tag that adds extra post-summon effects to a battle for a specific Pokemon * Tag that adds extra post-summon effects to a battle for a specific Pokemon.
* Currently used only in MysteryEncounters to provide start of fight stat buffs * These post-summon effects are performed through {@linkcode Pokemon.mysteryEncounterBattleEffects},
* and can be used to unshift special phases, etc.
* Currently used only in MysteryEncounters to provide start of fight stat buffs.
*/ */
export class MysteryEncounterPostSummonTag extends BattlerTag { export class MysteryEncounterPostSummonTag extends BattlerTag {
constructor(sourceMove: Moves) { constructor() {
super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1, sourceMove); super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -2228,13 +2230,11 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
const ret = super.lapse(pokemon, lapseType); const ret = super.lapse(pokemon, lapseType);
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.CUSTOM) {
// Give pokemon +1 stats for battle
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
const mysteryEncounterBattleEffects = pokemon.mysteryEncounterBattleEffects; if (pokemon.mysteryEncounterBattleEffects) {
if (mysteryEncounterBattleEffects) { pokemon.mysteryEncounterBattleEffects(pokemon);
mysteryEncounterBattleEffects(pokemon);
} }
} }
} }
@ -2407,7 +2407,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.GORILLA_TACTICS: case BattlerTagType.GORILLA_TACTICS:
return new GorillaTacticsTag(); return new GorillaTacticsTag();
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
return new MysteryEncounterPostSummonTag(sourceMove); return new MysteryEncounterPostSummonTag();
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -451,7 +451,7 @@ function doGreedentEatBerries(scene: BattleScene) {
/** /**
* *
* @param scene * @param scene
* @param isEat - default false. Will "create" pile when false, and remove pile when true. * @param isEat Default false. Will "create" pile when false, and remove pile when true.
*/ */
function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) { function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) {
const berryAddDelay = 150; const berryAddDelay = 150;

View File

@ -12,6 +12,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { GameOverPhase } from "#app/phases/game-over-phase";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounter:mysteriousChest"; const namespace = "mysteryEncounter:mysteriousChest";
@ -116,8 +117,8 @@ export const MysteriousChestEncounter: MysteryEncounter =
// Open the chest // Open the chest
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const roll = encounter.misc.roll; const roll = encounter.misc.roll;
if (roll > 60) { if (roll > 80) {
// Choose between 2 COMMON / 2 GREAT tier items (30%) // Choose between 2 COMMON / 2 GREAT tier items (20%)
setEncounterRewards(scene, { setEncounterRewards(scene, {
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.COMMON, ModifierTier.COMMON,
@ -129,8 +130,8 @@ export const MysteriousChestEncounter: MysteryEncounter =
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.normal`); queueEncounterMessage(scene, `${namespace}.option.1.normal`);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
} else if (roll > 40) { } else if (roll > 50) {
// Choose between 3 ULTRA tier items (20%) // Choose between 3 ULTRA tier items (30%)
setEncounterRewards(scene, { setEncounterRewards(scene, {
guaranteedModifierTiers: [ guaranteedModifierTiers: [
ModifierTier.ULTRA, ModifierTier.ULTRA,
@ -141,35 +142,38 @@ export const MysteriousChestEncounter: MysteryEncounter =
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.good`); queueEncounterMessage(scene, `${namespace}.option.1.good`);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
} else if (roll > 36) { } else if (roll > 40) {
// Choose between 2 ROGUE tier items (10%) // Choose between 2 ROGUE tier items (10%)
setEncounterRewards(scene, { setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE] });
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE],
});
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.great`); queueEncounterMessage(scene, `${namespace}.option.1.great`);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
} else if (roll > 35) { } else if (roll > 35) {
// Choose 1 MASTER tier item (5%) // Choose 1 MASTER tier item (5%)
setEncounterRewards(scene, { setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER] });
guaranteedModifierTiers: [ModifierTier.MASTER],
});
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.amazing`); queueEncounterMessage(scene, `${namespace}.option.1.amazing`);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
} else { } else {
// Your highest level unfainted Pokemon gets OHKO. Progress with no rewards (35%) // Your highest level unfainted Pokemon gets OHKO. Start battle against a Gimmighoul (35%)
const highestLevelPokemon = getHighestLevelPlayerPokemon( const highestLevelPokemon = getHighestLevelPlayerPokemon(
scene, scene,
true true
); );
koPlayerPokemon(scene, highestLevelPokemon); koPlayerPokemon(scene, highestLevelPokemon);
// Handle game over edge case
encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle());
// Show which Pokemon was KOed, then start battle against Gimmighoul if (allowedPokemon.length === 0) {
await showEncounterText(scene, `${namespace}.option.1.bad`); // If there are no longer any legal pokemon in the party, game over.
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); scene.clearPhaseQueue();
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); scene.unshiftPhase(new GameOverPhase(scene));
} else {
// Show which Pokemon was KOed, then start battle against Gimmighoul
encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender());
await showEncounterText(scene, `${namespace}.option.1.bad`);
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
}
} }
}) })
.build() .build()

View File

@ -2,21 +2,20 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounte
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils";
import { Type } from "../type"; import { Type } from "../type";
import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements";
import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
export type OptionPhaseCallback = (scene: BattleScene) => Promise<void | boolean>; export type OptionPhaseCallback = (scene: BattleScene) => Promise<void | boolean>;
/** /**
* Used by {@link MysteryEncounterOptionBuilder} class to define required/optional properties on the {@link MysteryEncounterOption} class when building. * Used by {@linkcode MysteryEncounterOptionBuilder} class to define required/optional properties on the {@linkcode MysteryEncounterOption} class when building.
* *
* Should ONLY contain properties that are necessary for {@link MysteryEncounterOption} construction. * Should ONLY contain properties that are necessary for {@linkcode MysteryEncounterOption} construction.
* Post-construct and flag data properties are defined in the {@link MysteryEncounterOption} class itself. * Post-construct and flag data properties are defined in the {@linkcode MysteryEncounterOption} class itself.
*/ */
export interface IMysteryEncounterOption { export interface IMysteryEncounterOption {
optionMode: MysteryEncounterOptionMode; optionMode: MysteryEncounterOptionMode;
@ -66,31 +65,48 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ?? []; this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ?? [];
} }
hasRequirements() { /**
* Returns true if option contains any {@linkcode EncounterRequirement}s, false otherwise.
*/
hasRequirements(): boolean {
return this.requirements.length > 0 || this.primaryPokemonRequirements.length > 0 || this.secondaryPokemonRequirements.length > 0; return this.requirements.length > 0 || this.primaryPokemonRequirements.length > 0 || this.secondaryPokemonRequirements.length > 0;
} }
meetsRequirements(scene: BattleScene) { /**
return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) && * Returns true if all {@linkcode EncounterRequirement}s for the option are met
this.meetsSupportingRequirementAndSupportingPokemonSelected(scene) && * @param scene
this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); */
meetsRequirements(scene: BattleScene): boolean {
return !this.requirements.some(requirement => !requirement.meetsRequirement(scene))
&& this.meetsSupportingRequirementAndSupportingPokemonSelected(scene)
&& this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene);
} }
pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon) { /**
* Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met
* @param scene
* @param pokemon
*/
pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean {
return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id));
} }
meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene) { /**
* Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met,
* AND there is a valid Pokemon assigned to {@linkcode primaryPokemon}.
* If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined,
* can cause scenarios where there are not enough Pokemon that are sufficient for all requirements.
* @param scene
*/
meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean {
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
return true; return true;
} }
let qualified: PlayerPokemon[] = scene.getParty(); let qualified: PlayerPokemon[] = scene.getParty();
for (const req of this.primaryPokemonRequirements) { for (const req of this.primaryPokemonRequirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
if (req instanceof EncounterPokemonRequirement) { const queryParty = req.queryParty(scene.getParty());
const queryParty = req.queryParty(scene.getParty()); qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
}
} else { } else {
this.primaryPokemon = undefined; this.primaryPokemon = undefined;
return false; return false;
@ -114,13 +130,12 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
} }
if (truePrimaryPool.length > 0) { if (truePrimaryPool.length > 0) {
// always choose from the non-overlapping pokemon first // always choose from the non-overlapping pokemon first
this.primaryPokemon = truePrimaryPool[Utils.randSeedInt(truePrimaryPool.length, 0)]; this.primaryPokemon = truePrimaryPool[randSeedInt(truePrimaryPool.length)];
return true; return true;
} else { } else {
// if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool // if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool
if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) { if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) {
// is this working? this.primaryPokemon = overlap[randSeedInt(overlap.length)];
this.primaryPokemon = overlap[Utils.randSeedInt(overlap.length, 0)];
this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon); this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon);
return true; return true;
} }
@ -134,7 +149,14 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
} }
} }
meetsSupportingRequirementAndSupportingPokemonSelected(scene: BattleScene) { /**
* Returns true if all SECONDARY {@linkcode EncounterRequirement}s for the option are met,
* AND there is a valid Pokemon assigned to {@linkcode secondaryPokemon} (if applicable).
* If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined,
* can cause scenarios where there are not enough Pokemon that are sufficient for all requirements.
* @param scene
*/
meetsSupportingRequirementAndSupportingPokemonSelected(scene: BattleScene): boolean {
if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) {
this.secondaryPokemon = []; this.secondaryPokemon = [];
return true; return true;
@ -143,10 +165,8 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
let qualified: PlayerPokemon[] = scene.getParty(); let qualified: PlayerPokemon[] = scene.getParty();
for (const req of this.secondaryPokemonRequirements) { for (const req of this.secondaryPokemonRequirements) {
if (req.meetsRequirement(scene)) { if (req.meetsRequirement(scene)) {
if (req instanceof EncounterPokemonRequirement) { const queryParty = req.queryParty(scene.getParty());
const queryParty = req.queryParty(scene.getParty()); qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
}
} else { } else {
this.secondaryPokemon = []; this.secondaryPokemon = [];
return false; return false;
@ -175,6 +195,10 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
return Object.assign(this, { hasDexProgress: hasDexProgress }); return Object.assign(this, { hasDexProgress: hasDexProgress });
} }
/**
* Adds a {@linkcode EncounterSceneRequirement} to {@linkcode requirements}
* @param requirement
*/
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<IMysteryEncounterOption, "requirements">> { withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<IMysteryEncounterOption, "requirements">> {
if (requirement instanceof EncounterPokemonRequirement) { if (requirement instanceof EncounterPokemonRequirement) {
Error("Incorrectly added pokemon requirement as scene requirement."); Error("Incorrectly added pokemon requirement as scene requirement.");
@ -188,10 +212,20 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
return this.withSceneRequirement(new MoneyRequirement(requiredMoney, scalingMultiplier)); return this.withSceneRequirement(new MoneyRequirement(requiredMoney, scalingMultiplier));
} }
/**
* Defines logic that runs immediately when an option is selected, but before the Encounter continues.
* Can determine whether or not the Encounter *should* continue.
* If there are scenarios where the Encounter should NOT continue, should return boolean instead of void.
* @param onPreOptionPhase
*/
withPreOptionPhase(onPreOptionPhase: OptionPhaseCallback): this & Required<Pick<IMysteryEncounterOption, "onPreOptionPhase">> { withPreOptionPhase(onPreOptionPhase: OptionPhaseCallback): this & Required<Pick<IMysteryEncounterOption, "onPreOptionPhase">> {
return Object.assign(this, { onPreOptionPhase: onPreOptionPhase }); return Object.assign(this, { onPreOptionPhase: onPreOptionPhase });
} }
/**
* MUST be defined by every {@linkcode MysteryEncounterOption}
* @param onOptionPhase
*/
withOptionPhase(onOptionPhase: OptionPhaseCallback): this & Required<Pick<IMysteryEncounterOption, "onOptionPhase">> { withOptionPhase(onOptionPhase: OptionPhaseCallback): this & Required<Pick<IMysteryEncounterOption, "onOptionPhase">> {
return Object.assign(this, { onOptionPhase: onOptionPhase }); return Object.assign(this, { onOptionPhase: onOptionPhase });
} }
@ -200,6 +234,10 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
return Object.assign(this, { onPostOptionPhase: onPostOptionPhase }); return Object.assign(this, { onPostOptionPhase: onPostOptionPhase });
} }
/**
* Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode primaryPokemonRequirements}
* @param requirement
*/
withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<IMysteryEncounterOption, "primaryPokemonRequirements">> { withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<IMysteryEncounterOption, "primaryPokemonRequirements">> {
if (requirement instanceof EncounterSceneRequirement) { if (requirement instanceof EncounterSceneRequirement) {
Error("Incorrectly added scene requirement as pokemon requirement."); Error("Incorrectly added scene requirement as pokemon requirement.");
@ -233,6 +271,11 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
return this.withPrimaryPokemonRequirement(new CanLearnMoveRequirement(move, options)); return this.withPrimaryPokemonRequirement(new CanLearnMoveRequirement(move, options));
} }
/**
* Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode secondaryPokemonRequirements}
* @param requirement
* @param excludePrimaryFromSecondaryRequirements
*/
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = true): this & Required<Pick<IMysteryEncounterOption, "secondaryPokemonRequirements">> { withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = true): this & Required<Pick<IMysteryEncounterOption, "secondaryPokemonRequirements">> {
if (requirement instanceof EncounterSceneRequirement) { if (requirement instanceof EncounterSceneRequirement) {
Error("Incorrectly added scene requirement as pokemon requirement."); Error("Incorrectly added scene requirement as pokemon requirement.");
@ -244,7 +287,7 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
} }
/** /**
* Se the full dialogue object to the option. Will override anything already set * Set the full dialogue object to the option. Will override anything already set
* *
* @param dialogue see {@linkcode OptionTextDisplay} * @param dialogue see {@linkcode OptionTextDisplay}
* @returns * @returns

View File

@ -157,7 +157,7 @@ export class WaveRangeRequirement extends EncounterSceneRequirement {
/** /**
* Used for specifying a unique wave or wave range requirement * Used for specifying a unique wave or wave range requirement
* If minWaveIndex and maxWaveIndex are equivalent, will check for exact wave number * If minWaveIndex and maxWaveIndex are equivalent, will check for exact wave number
* @param waveRange - [min, max] * @param waveRange [min, max]
*/ */
constructor(waveRange: [number, number]) { constructor(waveRange: [number, number]) {
super(); super();
@ -165,9 +165,9 @@ export class WaveRangeRequirement extends EncounterSceneRequirement {
} }
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
if (!isNullOrUndefined(this?.waveRange) && this.waveRange?.[0] <= this.waveRange?.[1]) { if (!isNullOrUndefined(this.waveRange) && this.waveRange?.[0] <= this.waveRange?.[1]) {
const waveIndex = scene.currentBattle.waveIndex; const waveIndex = scene.currentBattle.waveIndex;
if (waveIndex >= 0 && (this?.waveRange?.[0] >= 0 && this.waveRange?.[0] > waveIndex) || (this?.waveRange?.[1] >= 0 && this.waveRange?.[1] < waveIndex)) { if (waveIndex >= 0 && (this.waveRange?.[0] >= 0 && this.waveRange?.[0] > waveIndex) || (this.waveRange?.[1] >= 0 && this.waveRange?.[1] < waveIndex)) {
return false; return false;
} }
} }
@ -186,8 +186,8 @@ export class WaveModulusRequirement extends EncounterSceneRequirement {
/** /**
* Used for specifying a modulus requirement on the wave index * Used for specifying a modulus requirement on the wave index
* For example, can be used to require the wave index to end with 1, 2, or 3 * For example, can be used to require the wave index to end with 1, 2, or 3
* @param waveModuli - number[], the allowed modulus results * @param waveModuli The allowed modulus results
* @param modulusValue - number, the modulus calculation value * @param modulusValue The modulus calculation value
* *
* Example: * Example:
* new WaveModulusRequirement([1, 2, 3], 10) will check for 1st/2nd/3rd waves that are immediately after a multiple of 10 wave * new WaveModulusRequirement([1, 2, 3], 10) will check for 1st/2nd/3rd waves that are immediately after a multiple of 10 wave
@ -218,7 +218,7 @@ export class TimeOfDayRequirement extends EncounterSceneRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const timeOfDay = scene.arena?.getTimeOfDay(); const timeOfDay = scene.arena?.getTimeOfDay();
if (!isNullOrUndefined(timeOfDay) && this?.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)) { if (!isNullOrUndefined(timeOfDay) && this.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)) {
return false; return false;
} }
@ -240,7 +240,7 @@ export class WeatherRequirement extends EncounterSceneRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const currentWeather = scene.arena.weather?.weatherType; const currentWeather = scene.arena.weather?.weatherType;
if (!isNullOrUndefined(currentWeather) && this?.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather!)) { if (!isNullOrUndefined(currentWeather) && this.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather!)) {
return false; return false;
} }
@ -264,7 +264,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
/** /**
* Used for specifying a party size requirement * Used for specifying a party size requirement
* If min and max are equivalent, will check for exact size * If min and max are equivalent, will check for exact size
* @param partySizeRange - [min, max] * @param partySizeRange
* @param excludeFainted * @param excludeFainted
*/ */
constructor(partySizeRange: [number, number], excludeFainted: boolean) { constructor(partySizeRange: [number, number], excludeFainted: boolean) {
@ -274,9 +274,9 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
} }
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
if (!isNullOrUndefined(this?.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) { if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) {
const partySize = this.excludeFainted ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; const partySize = this.excludeFainted ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length;
if (partySize >= 0 && (this?.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this?.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) { if (partySize >= 0 && (this.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) {
return false; return false;
} }
} }
@ -301,7 +301,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) {
return false; return false;
} }
let modifierCount = 0; let modifierCount = 0;
@ -364,7 +364,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredSpecies?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -402,7 +402,7 @@ export class NatureRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredNature?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -486,7 +486,7 @@ export class MoveRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -530,7 +530,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -570,7 +570,7 @@ export class EvolutionTargetSpeciesRequirement extends EncounterPokemonRequireme
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionTargetSpecies?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionTargetSpecies?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -609,7 +609,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredAbilities?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -646,7 +646,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredStatusEffect?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) {
return false; return false;
} }
const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -716,7 +716,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredFormChangeItem?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -768,7 +768,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionItem?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;

View File

@ -26,10 +26,10 @@ export interface EncounterStartOfBattleEffect {
} }
/** /**
* Used by {@link MysteryEncounterBuilder} class to define required/optional properties on the {@link MysteryEncounter} class when building. * Used by {@linkcode MysteryEncounterBuilder} class to define required/optional properties on the {@linkcode MysteryEncounter} class when building.
* *
* Should ONLY contain properties that are necessary for {@link MysteryEncounter} construction. * Should ONLY contain properties that are necessary for {@linkcode MysteryEncounter} construction.
* Post-construct and flag data properties are defined in the {@link MysteryEncounter} class itself. * Post-construct and flag data properties are defined in the {@linkcode MysteryEncounter} class itself.
*/ */
export interface IMysteryEncounter { export interface IMysteryEncounter {
encounterType: MysteryEncounterType; encounterType: MysteryEncounterType;
@ -124,12 +124,12 @@ export default class MysteryEncounter implements IMysteryEncounter {
maxAllowedEncounters: number; maxAllowedEncounters: number;
/** /**
* If true, encounter will not animate the target Pokemon as part of battle animations * If true, encounter will not animate the target Pokemon as part of battle animations
* Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example)
*/ */
hasBattleAnimationsWithoutTargets: boolean; hasBattleAnimationsWithoutTargets: boolean;
/** /**
* If true, will skip enemy pokemon turns during battle for the encounter * If true, will skip enemy pokemon turns during battle for the encounter
* Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example)
*/ */
skipEnemyBattleTurns: boolean; skipEnemyBattleTurns: boolean;
/** /**
@ -144,9 +144,9 @@ export default class MysteryEncounter implements IMysteryEncounter {
onInit?: (scene: BattleScene) => boolean; onInit?: (scene: BattleScene) => boolean;
/** Event when battlefield visuals have finished sliding in and the encounter dialogue begins */ /** Event when battlefield visuals have finished sliding in and the encounter dialogue begins */
onVisualsStart?: (scene: BattleScene) => boolean; onVisualsStart?: (scene: BattleScene) => boolean;
/** Event triggered prior to {@link CommandPhase}, during {@link TurnInitPhase} */ /** Event triggered prior to {@linkcode CommandPhase}, during {@linkcode TurnInitPhase} */
onTurnStart?: (scene: BattleScene) => boolean; onTurnStart?: (scene: BattleScene) => boolean;
/** Event prior to any rewards logic in {@link MysteryEncounterRewardsPhase} */ /** Event prior to any rewards logic in {@linkcode MysteryEncounterRewardsPhase} */
onRewards?: (scene: BattleScene) => Promise<void>; onRewards?: (scene: BattleScene) => Promise<void>;
/** Will provide the player party EXP before rewards are displayed for that wave */ /** Will provide the player party EXP before rewards are displayed for that wave */
doEncounterExp?: (scene: BattleScene) => boolean; doEncounterExp?: (scene: BattleScene) => boolean;
@ -279,7 +279,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* @param scene * @param scene
* @returns * @returns
*/ */
meetsRequirements(scene: BattleScene) { meetsRequirements(scene: BattleScene): boolean {
const sceneReq = !this.requirements.some(requirement => !requirement.meetsRequirement(scene)); const sceneReq = !this.requirements.some(requirement => !requirement.meetsRequirement(scene));
const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary
const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene);
@ -293,10 +293,17 @@ export default class MysteryEncounter implements IMysteryEncounter {
* @param scene * @param scene
* @param pokemon * @param pokemon
*/ */
pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon) { pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean {
return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id));
} }
/**
* Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met,
* AND there is a valid Pokemon assigned to {@linkcode primaryPokemon}.
* If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined,
* can cause scenarios where there are not enough Pokemon that are sufficient for all requirements.
* @param scene
*/
private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean {
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
const activeMon = scene.getParty().filter(p => p.isActive(true)); const activeMon = scene.getParty().filter(p => p.isActive(true));
@ -354,6 +361,13 @@ export default class MysteryEncounter implements IMysteryEncounter {
} }
} }
/**
* Returns true if all SECONDARY {@linkcode EncounterRequirement}s for the option are met,
* AND there is a valid Pokemon assigned to {@linkcode secondaryPokemon} (if applicable).
* If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined,
* can cause scenarios where there are not enough Pokemon that are sufficient for all requirements.
* @param scene
*/
private meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean { private meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean {
if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) {
this.secondaryPokemon = []; this.secondaryPokemon = [];
@ -377,7 +391,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* Initializes encounter intro sprites based on the sprite configs defined in spriteConfigs * Initializes encounter intro sprites based on the sprite configs defined in spriteConfigs
* @param scene * @param scene
*/ */
initIntroVisuals(scene: BattleScene) { initIntroVisuals(scene: BattleScene): void {
this.introVisuals = new MysteryEncounterIntroVisuals(scene, this); this.introVisuals = new MysteryEncounterIntroVisuals(scene, this);
} }
@ -386,7 +400,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* Will use the first support pokemon in list * Will use the first support pokemon in list
* For multiple support pokemon in the dialogue token, it will have to be overridden. * For multiple support pokemon in the dialogue token, it will have to be overridden.
*/ */
populateDialogueTokensFromRequirements(scene: BattleScene) { populateDialogueTokensFromRequirements(scene: BattleScene): void {
this.meetsRequirements(scene); this.meetsRequirements(scene);
if (this.requirements?.length > 0) { if (this.requirements?.length > 0) {
for (const req of this.requirements) { for (const req of this.requirements) {
@ -461,7 +475,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
/** /**
* Used to cache a dialogue token for the encounter. * Used to cache a dialogue token for the encounter.
* Tokens will be auto-injected via the `{{key}}` pattern with `value`, * Tokens will be auto-injected via the `{{key}}` pattern with `value`,
* when using the {@link showEncounterText} and {@link showEncounterDialogue} helper functions. * when using the {@linkcode showEncounterText} and {@linkcode showEncounterDialogue} helper functions.
* *
* @param key * @param key
* @param value * @param value
@ -471,10 +485,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
} }
/** /**
* If an encounter uses {@link MysteryEncounterMode.continuousEncounter}, * If an encounter uses {@linkcode MysteryEncounterMode.continuousEncounter},
* should rely on this value for seed offset instead of wave index. * should rely on this value for seed offset instead of wave index.
* *
* This offset is incremented for each new {@link MysteryEncounterPhase} that occurs, * This offset is incremented for each new {@linkcode MysteryEncounterPhase} that occurs,
* so multi-encounter RNG will be consistent on resets and not be affected by number of turns, move RNG, etc. * so multi-encounter RNG will be consistent on resets and not be affected by number of turns, move RNG, etc.
*/ */
getSeedOffset() { getSeedOffset() {
@ -539,7 +553,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* Use for complex options. * Use for complex options.
* There should be at least 2 options defined and no more than 4. * There should be at least 2 options defined and no more than 4.
* *
* @param option - MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance * @param option MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance
* @returns * @returns
*/ */
withOption(option: MysteryEncounterOption): this & Pick<IMysteryEncounter, "options"> { withOption(option: MysteryEncounterOption): this & Pick<IMysteryEncounter, "options"> {
@ -558,9 +572,8 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* There should be at least 2 options defined and no more than 4. * There should be at least 2 options defined and no more than 4.
* If complex use {@linkcode MysteryEncounterBuilder.withOption} * If complex use {@linkcode MysteryEncounterBuilder.withOption}
* *
* @param hasDexProgress - * @param dialogue {@linkcode OptionTextDisplay}
* @param dialogue - {@linkcode OptionTextDisplay} * @param callback {@linkcode OptionPhaseCallback}
* @param callback - {@linkcode OptionPhaseCallback}
* @returns * @returns
*/ */
withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick<IMysteryEncounter, "options"> { withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick<IMysteryEncounter, "options"> {
@ -658,7 +671,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
/** /**
* If true, encounter will not animate the target Pokemon as part of battle animations * If true, encounter will not animate the target Pokemon as part of battle animations
* Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example)
* Default false * Default false
* @param hasBattleAnimationsWithoutTargets * @param hasBattleAnimationsWithoutTargets
*/ */
@ -668,7 +681,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
/** /**
* If true, encounter will not animate the target Pokemon as part of battle animations * If true, encounter will not animate the target Pokemon as part of battle animations
* Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example)
* Default false * Default false
* @param skipEnemyBattleTurns * @param skipEnemyBattleTurns
*/ */

View File

@ -30,20 +30,18 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
super(); super();
this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves]; this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves];
const { excludeLevelMoves, excludeTmMoves, excludeEggMoves, includeFainted, minNumberOfPokemon, invertQuery } = options; this.excludeLevelMoves = options.excludeLevelMoves ?? false;
this.excludeTmMoves = options.excludeTmMoves ?? false;
this.excludeLevelMoves = excludeLevelMoves ?? false; this.excludeEggMoves = options.excludeEggMoves ?? false;
this.excludeTmMoves = excludeTmMoves ?? false; this.includeFainted = options.includeFainted ?? false;
this.excludeEggMoves = excludeEggMoves ?? false; this.minNumberOfPokemon = options.minNumberOfPokemon ?? 1;
this.includeFainted = includeFainted ?? false; this.invertQuery = options.invertQuery ?? false;
this.minNumberOfPokemon = minNumberOfPokemon ?? 1;
this.invertQuery = invertQuery ?? false;
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle())); const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle()));
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) { if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false; return false;
} }

View File

@ -5,11 +5,11 @@ import { isNullOrUndefined } from "#app/utils";
import i18next from "i18next"; import i18next from "i18next";
/** /**
* Will inject all relevant dialogue tokens that exist in the {@link BattleScene.currentBattle.mysteryEncounter.dialogueTokens}, into i18n text. * Will inject all relevant dialogue tokens that exist in the {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens}, into i18n text.
* Also adds BBCodeText fragments for colored text, if applicable * Also adds BBCodeText fragments for colored text, if applicable
* @param scene * @param scene
* @param keyOrString * @param keyOrString
* @param primaryStyle - can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly * @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly
* @param uiTheme * @param uiTheme
*/ */
export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string | null { export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string | null {
@ -17,7 +17,7 @@ export function getEncounterText(scene: BattleScene, keyOrString?: string, prima
return null; return null;
} }
let textString: string | null = getTextWithDialogueTokens(scene, keyOrString); let textString: string | null = getTextWithDialogueTokens(scene, keyOrString!);
// Can only color the text if a Primary Style is defined // Can only color the text if a Primary Style is defined
// primaryStyle is applied to all text that does not have its own specified style // primaryStyle is applied to all text that does not have its own specified style
@ -29,22 +29,15 @@ export function getEncounterText(scene: BattleScene, keyOrString?: string, prima
} }
/** /**
* Helper function to inject {@link BattleScene.currentBattle.mysteryEncounter.dialogueTokens} into a given content string * Helper function to inject {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens} into a given content string
* @param scene * @param scene
* @param keyOrString * @param keyOrString
*/ */
function getTextWithDialogueTokens(scene: BattleScene, keyOrString?: string): string | null { function getTextWithDialogueTokens(scene: BattleScene, keyOrString: string): string | null {
if (isNullOrUndefined(keyOrString)) {
return null;
}
const tokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; const tokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens;
// @ts-ignore
if (i18next.exists(keyOrString, tokens)) { if (i18next.exists(keyOrString, tokens)) {
const stringArray = [`${keyOrString}`] as any; return i18next.t(keyOrString, tokens) as string;
stringArray.raw = [`${keyOrString}`];
// @ts-ignore
return i18next.t(stringArray, tokens) as string;
} }
return keyOrString ?? null; return keyOrString ?? null;

View File

@ -101,8 +101,8 @@ export interface EnemyPartyConfig {
* Generates an enemy party for a mystery encounter battle * Generates an enemy party for a mystery encounter battle
* This will override and replace any standard encounter generation logic * This will override and replace any standard encounter generation logic
* Useful for tailoring specific battles to mystery encounters * Useful for tailoring specific battles to mystery encounters
* @param scene - Battle Scene * @param scene Battle Scene
* @param partyConfig - Can pass various customizable attributes for the enemy party, see EnemyPartyConfig * @param partyConfig Can pass various customizable attributes for the enemy party, see EnemyPartyConfig
*/ */
export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: EnemyPartyConfig): Promise<void> { export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: EnemyPartyConfig): Promise<void> {
const loaded: boolean = false; const loaded: boolean = false;
@ -352,7 +352,7 @@ export function loadCustomMovesForEncounter(scene: BattleScene, moves: Moves | M
/** /**
* Will update player money, and animate change (sound optional) * Will update player money, and animate change (sound optional)
* @param scene - Battle Scene * @param scene
* @param changeValue * @param changeValue
* @param playSound * @param playSound
* @param showMessage * @param showMessage
@ -375,9 +375,9 @@ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playS
/** /**
* Converts modifier bullshit to an actual item * Converts modifier bullshit to an actual item
* @param scene - Battle Scene * @param scene Battle Scene
* @param modifier * @param modifier
* @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. * @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc.
*/ */
export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType | null { export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType | null {
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier);
@ -805,10 +805,10 @@ export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) {
} }
/** /**
* Can queue extra phases or logic during {@link TurnInitPhase} * Can queue extra phases or logic during {@linkcode TurnInitPhase}
* Should mostly just be used for injecting custom phases into the battle system on turn start * Should mostly just be used for injecting custom phases into the battle system on turn start
* @param scene * @param scene
* @return boolean - if true, will skip the remainder of the {@link TurnInitPhase} * @return boolean - if true, will skip the remainder of the {@linkcode TurnInitPhase}
*/ */
export function handleMysteryEncounterTurnStartEffects(scene: BattleScene): boolean { export function handleMysteryEncounterTurnStartEffects(scene: BattleScene): boolean {
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;

View File

@ -50,8 +50,8 @@ export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string,
* Will never remove the player's last non-fainted Pokemon (if they only have 1) * Will never remove the player's last non-fainted Pokemon (if they only have 1)
* Otherwise, picks a Pokemon completely at random and removes from the party * Otherwise, picks a Pokemon completely at random and removes from the party
* @param scene * @param scene
* @param isAllowedInBattle - default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon * @param isAllowedInBattle Default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon
* @param doNotReturnLastAbleMon - If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) * @param doNotReturnLastAbleMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted)
* @returns * @returns
*/ */
export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon { export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon {
@ -78,7 +78,7 @@ export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: bo
/** /**
* Ties are broken by whatever mon is closer to the front of the party * Ties are broken by whatever mon is closer to the front of the party
* @param scene * @param scene
* @param unfainted - default false. If true, only picks from unfainted mons. * @param unfainted Default false. If true, only picks from unfainted mons.
* @returns * @returns
*/ */
export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon {
@ -99,8 +99,8 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: bool
/** /**
* Ties are broken by whatever mon is closer to the front of the party * Ties are broken by whatever mon is closer to the front of the party
* @param scene * @param scene
* @param stat - stat to search for * @param stat Stat to search for
* @param unfainted - default false. If true, only picks from unfainted mons. * @param unfainted Default false. If true, only picks from unfainted mons.
* @returns * @returns
*/ */
export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, unfainted: boolean = false): PlayerPokemon { export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, unfainted: boolean = false): PlayerPokemon {
@ -218,7 +218,7 @@ export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) {
/** /**
* Handles applying hp changes to a player pokemon. * Handles applying hp changes to a player pokemon.
* Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info. * Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info.
* TODO: handle special cases like wonder-guard/ninjask * TODO: should we handle special cases like wonder-guard/shedinja?
* @param scene the battle scene * @param scene the battle scene
* @param pokemon the player pokemon to apply the hp change to * @param pokemon the player pokemon to apply the hp change to
* @param value the hp change amount. Positive for heal. Negative for damage * @param value the hp change amount. Positive for heal. Negative for damage
@ -258,7 +258,7 @@ export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon,
*/ */
export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) { export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) {
if (heal <= 0) { if (heal <= 0) {
console.warn("Damaging pokemong with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead."); console.warn("Damaging pokemon with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead.");
} }
applyHpChangeToPokemon(scene, pokemon, heal); applyHpChangeToPokemon(scene, pokemon, heal);

View File

@ -263,7 +263,7 @@ function doCircleInward(scene: BattleScene, transformationBaseBg: Phaser.GameObj
} }
/** /**
* Helper function for {@link doSpiralUpward}, handles a single particle * Helper function for {@linkcode doSpiralUpward}, handles a single particle
* @param scene * @param scene
* @param trigIndex * @param trigIndex
* @param transformationBaseBg * @param transformationBaseBg
@ -308,7 +308,7 @@ function doSpiralUpwardParticle(scene: BattleScene, trigIndex: number, transform
} }
/** /**
* Helper function for {@link doArcDownward}, handles a single particle * Helper function for {@linkcode doArcDownward}, handles a single particle
* @param scene * @param scene
* @param trigIndex * @param trigIndex
* @param transformationBaseBg * @param transformationBaseBg

View File

@ -227,6 +227,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
if (!legalPlayerPokemon.length) { if (!legalPlayerPokemon.length) {
this.scene.unshiftPhase(new GameOverPhase(this.scene)); this.scene.unshiftPhase(new GameOverPhase(this.scene));
return this.end();
} }
// Check for any KOd player mons and switch // Check for any KOd player mons and switch

View File

@ -24,6 +24,8 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { BerryModifier } from "#app/modifier/modifier"; import { BerryModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import * as TextUtils from "#app/ui/text";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
const namespace = "mysteryEncounter:uncommonBreed"; const namespace = "mysteryEncounter:uncommonBreed";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -167,7 +169,10 @@ describe("Uncommon Breed - Mystery Encounter", () => {
}); });
}); });
it("should NOT be selectable if the player doesn't have enough berries", { retry: 5 }, async () => { it("should NOT be selectable if the player doesn't have enough berries", async () => {
// For some reason, BBCodeText has issues with this test
vi.spyOn(TextUtils, "addBBCodeTextObject").mockImplementation(() => new BBCodeText(scene, 0, 0, "test"));
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
// Clear out any pesky mods that slipped through test spin-up // Clear out any pesky mods that slipped through test spin-up
scene.modifiers.forEach(mod => { scene.modifiers.forEach(mod => {

View File

@ -189,8 +189,8 @@ export default class GameManager {
/** /**
* Runs the game to a mystery encounter phase. * Runs the game to a mystery encounter phase.
* @param encounterType - if specified, will expect encounter to have been spawned * @param encounterType if specified, will expect encounter to have been spawned
* @param species - Optional array of species for party. * @param species Optional array of species for party.
* @returns A promise that resolves when the EncounterPhase ends. * @returns A promise that resolves when the EncounterPhase ends.
*/ */
async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) { async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) {

View File

@ -17,9 +17,7 @@ export default class MockText implements MockGameObject {
this.scene = textureManager.scene; this.scene = textureManager.scene;
this.textureManager = textureManager; this.textureManager = textureManager;
this.style = {}; this.style = {};
// DO NOT REMOVE: function needs to be stubbed for tests // Phaser.GameObjects.TextStyle.prototype.setStyle = () => this;
// @ts-ignore
Phaser.GameObjects.TextStyle.prototype.setStyle = () => this;
// Phaser.GameObjects.Text.prototype.updateText = () => null; // Phaser.GameObjects.Text.prototype.updateText = () => null;
// Phaser.Textures.TextureManager.prototype.addCanvas = () => {}; // Phaser.Textures.TextureManager.prototype.addCanvas = () => {};
UI.prototype.showText = this.showText; UI.prototype.showText = this.showText;

View File

@ -104,7 +104,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.dexProgressContainer.setVisible(true); this.dexProgressContainer.setVisible(true);
this.displayEncounterOptions(slideInDescription); this.displayEncounterOptions(slideInDescription);
const cursor = this.getCursor(); const cursor = this.getCursor();
if (cursor === (this?.optionsContainer?.length || 0) - 1) { if (cursor === (this.optionsContainer?.length || 0) - 1) {
// Always resets cursor on view party button if it was last there // Always resets cursor on view party button if it was last there
this.setCursor(cursor); this.setCursor(cursor);
} else { } else {

View File

@ -227,7 +227,7 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui
} }
/** /**
* Should only be used with BBCodeText (see addBBCodeTextObject()) * Should only be used with BBCodeText (see {@linkcode addBBCodeTextObject()})
* This does NOT work with UI showText() or showDialogue() methods. * This does NOT work with UI showText() or showDialogue() methods.
* Method will do pattern match/replace and apply BBCode color/shadow styling to substrings within the content: * Method will do pattern match/replace and apply BBCode color/shadow styling to substrings within the content:
* @[<TextStyle>]{<text to color>} * @[<TextStyle>]{<text to color>}
@ -236,8 +236,8 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui
* - "blue text" with TextStyle.SUMMARY_BLUE applied * - "blue text" with TextStyle.SUMMARY_BLUE applied
* - " primaryStyle text " with primaryStyle TextStyle applied * - " primaryStyle text " with primaryStyle TextStyle applied
* - "red text" with TextStyle.SUMMARY_RED applied * - "red text" with TextStyle.SUMMARY_RED applied
* @param content - string with styling that need to be applied for BBCodeTextObject * @param content string with styling that need to be applied for BBCodeTextObject
* @param primaryStyle - primary style is required in order to escape BBCode styling properly. * @param primaryStyle Primary style is required in order to escape BBCode styling properly.
* @param uiTheme * @param uiTheme
*/ */
export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string {