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
* @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) {
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
* @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 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 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 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.
*/
applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void {
const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds;
@ -3040,7 +3040,7 @@ export default class BattleScene extends SceneBase {
/**
* 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
*/
getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter {
@ -3111,10 +3111,11 @@ export default class BattleScene extends SceneBase {
return false;
}
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;
}
if (!encounterCandidate.meetsRequirements!(this)) { // Meets encounter requirements
if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements
return false;
}
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)];
// New encounter object to not dirty flags
encounter = new MysteryEncounter(encounter);
encounter.populateDialogueTokensFromRequirements!(this);
encounter.populateDialogueTokensFromRequirements(this);
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
* @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> {
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
* 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 startLoad
*/
@ -1079,7 +1079,6 @@ export abstract class BattleAnim {
[AnimFrameTarget.USER]: [],
[AnimFrameTarget.TARGET]: []
};
const spritePriorities: integer[] = [];
const cleanUpAndComplete = () => {
for (const ms of Object.values(spriteCache).flat()) {
@ -1104,8 +1103,8 @@ export abstract class BattleAnim {
this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ];
this.dstLine = [ 150, 75, targetInitialX, targetInitialY ];
let r = anim!.frames.length;
let f = 0;
let totalFrames = anim!.frames.length;
let frameCount = 0;
let existingFieldSprites = scene.field.getAll().slice(0);
@ -1114,11 +1113,9 @@ export abstract class BattleAnim {
repeat: anim!.frames.length,
onRepeat: () => {
existingFieldSprites = scene.field.getAll().slice(0);
const spriteFrames = anim!.frames[f];
const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[f], targetInitialX, targetInitialY);
const u = 0;
const t = 0;
let g = 0;
const spriteFrames = anim!.frames[frameCount];
const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[frameCount], targetInitialX, targetInitialY);
let graphicFrameCount = 0;
for (const frame of spriteFrames) {
if (frame.target !== AnimFrameTarget.GRAPHIC) {
console.log("Encounter animations do not support targets");
@ -1126,16 +1123,14 @@ export abstract class BattleAnim {
}
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);
sprites.push(newSprite);
scene.field.add(newSprite);
spritePriorities.push(1);
}
const graphicIndex = g++;
const graphicIndex = graphicFrameCount++;
const moveSprite = sprites[graphicIndex];
spritePriorities[graphicIndex] = frame.priority;
if (!isNullOrUndefined(frame.priority)) {
const setSpritePriority = (priority: integer) => {
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);
}
}
if (anim?.frameTimedEvents.get(f)) {
for (const event of anim.frameTimedEvents.get(f)!) {
r = Math.max((anim.frames.length - f) + event.execute(scene, this, frameTimedEventPriority), r);
if (anim?.frameTimedEvents.get(frameCount)) {
for (const event of anim.frameTimedEvents.get(frameCount)!) {
totalFrames = Math.max((anim.frames.length - frameCount) + event.execute(scene, this, frameTimedEventPriority), totalFrames);
}
}
const targets = Utils.getEnumValues(AnimFrameTarget);
for (const i of targets) {
const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t;
const count = graphicFrameCount;
if (count < spriteCache[i].length) {
const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length);
for (const rs of spritesToRemove) {
if (!rs.getData("locked") as boolean) {
const spriteCacheIndex = spriteCache[i].indexOf(rs);
for (const sprite of spritesToRemove) {
if (!sprite.getData("locked") as boolean) {
const spriteCacheIndex = spriteCache[i].indexOf(sprite);
spriteCache[i].splice(spriteCacheIndex, 1);
if (i === AnimFrameTarget.GRAPHIC) {
spritePriorities.splice(spriteCacheIndex, 1);
}
rs.destroy();
sprite.destroy();
}
}
}
}
f++;
r--;
frameCount++;
totalFrames--;
},
onComplete: () => {
for (const ms of Object.values(spriteCache).flat()) {
if (ms && !ms.getData("locked")) {
ms.destroy();
for (const sprite of Object.values(spriteCache).flat()) {
if (sprite && !sprite.getData("locked")) {
sprite.destroy();
}
}
if (r) {
if (totalFrames) {
scene.tweens.addCounter({
duration: Utils.getFrameMs(r),
duration: Utils.getFrameMs(totalFrames),
onComplete: () => cleanUpAndComplete()
});
} else {
@ -1277,7 +1269,7 @@ export class EncounterBattleAnim extends BattleAnim {
public 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.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
* Currently used only in MysteryEncounters to provide start of fight stat buffs
* Tag that adds extra post-summon effects to a battle for a specific Pokemon.
* 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 {
constructor(sourceMove: Moves) {
super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1, sourceMove);
constructor() {
super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1);
}
onAdd(pokemon: Pokemon): void {
@ -2228,13 +2230,11 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
const ret = super.lapse(pokemon, lapseType);
if (lapseType === BattlerTagLapseType.CUSTOM) {
// Give pokemon +1 stats for battle
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) {
const mysteryEncounterBattleEffects = pokemon.mysteryEncounterBattleEffects;
if (mysteryEncounterBattleEffects) {
mysteryEncounterBattleEffects(pokemon);
if (pokemon.mysteryEncounterBattleEffects) {
pokemon.mysteryEncounterBattleEffects(pokemon);
}
}
}
@ -2407,7 +2407,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
case BattlerTagType.GORILLA_TACTICS:
return new GorillaTacticsTag();
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
return new MysteryEncounterPostSummonTag(sourceMove);
return new MysteryEncounterPostSummonTag();
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -451,7 +451,7 @@ function doGreedentEatBerries(scene: BattleScene) {
/**
*
* @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) {
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 { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { GameOverPhase } from "#app/phases/game-over-phase";
/** i18n namespace for encounter */
const namespace = "mysteryEncounter:mysteriousChest";
@ -116,8 +117,8 @@ export const MysteriousChestEncounter: MysteryEncounter =
// Open the chest
const encounter = scene.currentBattle.mysteryEncounter!;
const roll = encounter.misc.roll;
if (roll > 60) {
// Choose between 2 COMMON / 2 GREAT tier items (30%)
if (roll > 80) {
// Choose between 2 COMMON / 2 GREAT tier items (20%)
setEncounterRewards(scene, {
guaranteedModifierTiers: [
ModifierTier.COMMON,
@ -129,8 +130,8 @@ export const MysteriousChestEncounter: MysteryEncounter =
// Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.normal`);
leaveEncounterWithoutBattle(scene);
} else if (roll > 40) {
// Choose between 3 ULTRA tier items (20%)
} else if (roll > 50) {
// Choose between 3 ULTRA tier items (30%)
setEncounterRewards(scene, {
guaranteedModifierTiers: [
ModifierTier.ULTRA,
@ -141,36 +142,39 @@ export const MysteriousChestEncounter: MysteryEncounter =
// Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.good`);
leaveEncounterWithoutBattle(scene);
} else if (roll > 36) {
} else if (roll > 40) {
// Choose between 2 ROGUE tier items (10%)
setEncounterRewards(scene, {
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE],
});
setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE] });
// Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.great`);
leaveEncounterWithoutBattle(scene);
} else if (roll > 35) {
// Choose 1 MASTER tier item (5%)
setEncounterRewards(scene, {
guaranteedModifierTiers: [ModifierTier.MASTER],
});
setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER] });
// Display result message then proceed to rewards
queueEncounterMessage(scene, `${namespace}.option.1.amazing`);
leaveEncounterWithoutBattle(scene);
} 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(
scene,
true
);
koPlayerPokemon(scene, highestLevelPokemon);
encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender());
// Handle game over edge case
const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle());
if (allowedPokemon.length === 0) {
// If there are no longer any legal pokemon in the party, game over.
scene.clearPhaseQueue();
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()
)

View File

@ -2,21 +2,20 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounte
import { Moves } from "#app/enums/moves";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils";
import { Type } from "../type";
import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements";
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";
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.
* Post-construct and flag data properties are defined in the {@link MysteryEncounterOption} class itself.
* Should ONLY contain properties that are necessary for {@linkcode MysteryEncounterOption} construction.
* Post-construct and flag data properties are defined in the {@linkcode MysteryEncounterOption} class itself.
*/
export interface IMysteryEncounterOption {
optionMode: MysteryEncounterOptionMode;
@ -66,31 +65,48 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
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;
}
meetsRequirements(scene: BattleScene) {
return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) &&
this.meetsSupportingRequirementAndSupportingPokemonSelected(scene) &&
this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene);
/**
* Returns true if all {@linkcode EncounterRequirement}s for the option are met
* @param 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));
}
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) {
return true;
}
let qualified: PlayerPokemon[] = scene.getParty();
for (const req of this.primaryPokemonRequirements) {
if (req.meetsRequirement(scene)) {
if (req instanceof EncounterPokemonRequirement) {
const queryParty = req.queryParty(scene.getParty());
qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
}
} else {
this.primaryPokemon = undefined;
return false;
@ -114,13 +130,12 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
}
if (truePrimaryPool.length > 0) {
// always choose from the non-overlapping pokemon first
this.primaryPokemon = truePrimaryPool[Utils.randSeedInt(truePrimaryPool.length, 0)];
this.primaryPokemon = truePrimaryPool[randSeedInt(truePrimaryPool.length)];
return true;
} else {
// 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)) {
// is this working?
this.primaryPokemon = overlap[Utils.randSeedInt(overlap.length, 0)];
this.primaryPokemon = overlap[randSeedInt(overlap.length)];
this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon);
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) {
this.secondaryPokemon = [];
return true;
@ -143,10 +165,8 @@ export default class MysteryEncounterOption implements IMysteryEncounterOption {
let qualified: PlayerPokemon[] = scene.getParty();
for (const req of this.secondaryPokemonRequirements) {
if (req.meetsRequirement(scene)) {
if (req instanceof EncounterPokemonRequirement) {
const queryParty = req.queryParty(scene.getParty());
qualified = qualified.filter(pkmn => queryParty.includes(pkmn));
}
} else {
this.secondaryPokemon = [];
return false;
@ -175,6 +195,10 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
return Object.assign(this, { hasDexProgress: hasDexProgress });
}
/**
* Adds a {@linkcode EncounterSceneRequirement} to {@linkcode requirements}
* @param requirement
*/
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<IMysteryEncounterOption, "requirements">> {
if (requirement instanceof EncounterPokemonRequirement) {
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));
}
/**
* 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">> {
return Object.assign(this, { onPreOptionPhase: onPreOptionPhase });
}
/**
* MUST be defined by every {@linkcode MysteryEncounterOption}
* @param onOptionPhase
*/
withOptionPhase(onOptionPhase: OptionPhaseCallback): this & Required<Pick<IMysteryEncounterOption, "onOptionPhase">> {
return Object.assign(this, { onOptionPhase: onOptionPhase });
}
@ -200,6 +234,10 @@ export class MysteryEncounterOptionBuilder implements Partial<IMysteryEncounterO
return Object.assign(this, { onPostOptionPhase: onPostOptionPhase });
}
/**
* Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode primaryPokemonRequirements}
* @param requirement
*/
withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<IMysteryEncounterOption, "primaryPokemonRequirements">> {
if (requirement instanceof EncounterSceneRequirement) {
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));
}
/**
* Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode secondaryPokemonRequirements}
* @param requirement
* @param excludePrimaryFromSecondaryRequirements
*/
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = true): this & Required<Pick<IMysteryEncounterOption, "secondaryPokemonRequirements">> {
if (requirement instanceof EncounterSceneRequirement) {
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}
* @returns

View File

@ -157,7 +157,7 @@ export class WaveRangeRequirement extends EncounterSceneRequirement {
/**
* Used for specifying a unique wave or wave range requirement
* If minWaveIndex and maxWaveIndex are equivalent, will check for exact wave number
* @param waveRange - [min, max]
* @param waveRange [min, max]
*/
constructor(waveRange: [number, number]) {
super();
@ -165,9 +165,9 @@ export class WaveRangeRequirement extends EncounterSceneRequirement {
}
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;
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;
}
}
@ -186,8 +186,8 @@ export class WaveModulusRequirement extends EncounterSceneRequirement {
/**
* 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
* @param waveModuli - number[], the allowed modulus results
* @param modulusValue - number, the modulus calculation value
* @param waveModuli The allowed modulus results
* @param modulusValue The modulus calculation value
*
* Example:
* 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 {
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;
}
@ -240,7 +240,7 @@ export class WeatherRequirement extends EncounterSceneRequirement {
meetsRequirement(scene: BattleScene): boolean {
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;
}
@ -264,7 +264,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
/**
* Used for specifying a party size requirement
* If min and max are equivalent, will check for exact size
* @param partySizeRange - [min, max]
* @param partySizeRange
* @param excludeFainted
*/
constructor(partySizeRange: [number, number], excludeFainted: boolean) {
@ -274,9 +274,9 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
}
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;
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;
}
}
@ -301,7 +301,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) {
return false;
}
let modifierCount = 0;
@ -364,7 +364,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredSpecies?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -402,7 +402,7 @@ export class NatureRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredNature?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -486,7 +486,7 @@ export class MoveRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -530,7 +530,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -570,7 +570,7 @@ export class EvolutionTargetSpeciesRequirement extends EncounterPokemonRequireme
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionTargetSpecies?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionTargetSpecies?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -609,7 +609,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredAbilities?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -646,7 +646,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredStatusEffect?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) {
return false;
}
const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -716,7 +716,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredFormChangeItem?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -768,7 +768,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement {
meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionItem?.length < 0) {
if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) {
return false;
}
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.
* Post-construct and flag data properties are defined in the {@link MysteryEncounter} class itself.
* Should ONLY contain properties that are necessary for {@linkcode MysteryEncounter} construction.
* Post-construct and flag data properties are defined in the {@linkcode MysteryEncounter} class itself.
*/
export interface IMysteryEncounter {
encounterType: MysteryEncounterType;
@ -124,12 +124,12 @@ export default class MysteryEncounter implements IMysteryEncounter {
maxAllowedEncounters: number;
/**
* 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;
/**
* 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;
/**
@ -144,9 +144,9 @@ export default class MysteryEncounter implements IMysteryEncounter {
onInit?: (scene: BattleScene) => boolean;
/** Event when battlefield visuals have finished sliding in and the encounter dialogue begins */
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;
/** Event prior to any rewards logic in {@link MysteryEncounterRewardsPhase} */
/** Event prior to any rewards logic in {@linkcode MysteryEncounterRewardsPhase} */
onRewards?: (scene: BattleScene) => Promise<void>;
/** Will provide the player party EXP before rewards are displayed for that wave */
doEncounterExp?: (scene: BattleScene) => boolean;
@ -279,7 +279,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* @param scene
* @returns
*/
meetsRequirements(scene: BattleScene) {
meetsRequirements(scene: BattleScene): boolean {
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 priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene);
@ -293,10 +293,17 @@ export default class MysteryEncounter implements IMysteryEncounter {
* @param scene
* @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));
}
/**
* 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 {
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
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 {
if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) {
this.secondaryPokemon = [];
@ -377,7 +391,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* Initializes encounter intro sprites based on the sprite configs defined in spriteConfigs
* @param scene
*/
initIntroVisuals(scene: BattleScene) {
initIntroVisuals(scene: BattleScene): void {
this.introVisuals = new MysteryEncounterIntroVisuals(scene, this);
}
@ -386,7 +400,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
* Will use the first support pokemon in list
* For multiple support pokemon in the dialogue token, it will have to be overridden.
*/
populateDialogueTokensFromRequirements(scene: BattleScene) {
populateDialogueTokensFromRequirements(scene: BattleScene): void {
this.meetsRequirements(scene);
if (this.requirements?.length > 0) {
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.
* 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 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.
*
* 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.
*/
getSeedOffset() {
@ -539,7 +553,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* Use for complex options.
* 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
*/
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.
* If complex use {@linkcode MysteryEncounterBuilder.withOption}
*
* @param hasDexProgress -
* @param dialogue - {@linkcode OptionTextDisplay}
* @param callback - {@linkcode OptionPhaseCallback}
* @param dialogue {@linkcode OptionTextDisplay}
* @param callback {@linkcode OptionPhaseCallback}
* @returns
*/
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
* 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
* @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
* 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
* @param skipEnemyBattleTurns
*/

View File

@ -30,20 +30,18 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
super();
this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves];
const { excludeLevelMoves, excludeTmMoves, excludeEggMoves, includeFainted, minNumberOfPokemon, invertQuery } = options;
this.excludeLevelMoves = excludeLevelMoves ?? false;
this.excludeTmMoves = excludeTmMoves ?? false;
this.excludeEggMoves = excludeEggMoves ?? false;
this.includeFainted = includeFainted ?? false;
this.minNumberOfPokemon = minNumberOfPokemon ?? 1;
this.invertQuery = invertQuery ?? false;
this.excludeLevelMoves = options.excludeLevelMoves ?? false;
this.excludeTmMoves = options.excludeTmMoves ?? false;
this.excludeEggMoves = options.excludeEggMoves ?? false;
this.includeFainted = options.includeFainted ?? false;
this.minNumberOfPokemon = options.minNumberOfPokemon ?? 1;
this.invertQuery = options.invertQuery ?? false;
}
override meetsRequirement(scene: BattleScene): boolean {
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;
}

View File

@ -5,11 +5,11 @@ import { isNullOrUndefined } from "#app/utils";
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
* @param scene
* @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
*/
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;
}
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
// 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 keyOrString
*/
function getTextWithDialogueTokens(scene: BattleScene, keyOrString?: string): string | null {
if (isNullOrUndefined(keyOrString)) {
return null;
}
function getTextWithDialogueTokens(scene: BattleScene, keyOrString: string): string | null {
const tokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens;
// @ts-ignore
if (i18next.exists(keyOrString, tokens)) {
const stringArray = [`${keyOrString}`] as any;
stringArray.raw = [`${keyOrString}`];
// @ts-ignore
return i18next.t(stringArray, tokens) as string;
return i18next.t(keyOrString, tokens) as string;
}
return keyOrString ?? null;

View File

@ -101,8 +101,8 @@ export interface EnemyPartyConfig {
* Generates an enemy party for a mystery encounter battle
* This will override and replace any standard encounter generation logic
* Useful for tailoring specific battles to mystery encounters
* @param scene - Battle Scene
* @param partyConfig - Can pass various customizable attributes for the enemy party, see EnemyPartyConfig
* @param scene Battle Scene
* @param partyConfig Can pass various customizable attributes for the enemy party, see EnemyPartyConfig
*/
export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: EnemyPartyConfig): Promise<void> {
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)
* @param scene - Battle Scene
* @param scene
* @param changeValue
* @param playSound
* @param showMessage
@ -375,9 +375,9 @@ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playS
/**
* Converts modifier bullshit to an actual item
* @param scene - Battle Scene
* @param scene Battle Scene
* @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 {
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
* @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 {
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)
* Otherwise, picks a Pokemon completely at random and removes from the party
* @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 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 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 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
*/
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
* @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
*/
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
* @param scene
* @param stat - stat to search for
* @param unfainted - default false. If true, only picks from unfainted mons.
* @param stat Stat to search for
* @param unfainted Default false. If true, only picks from unfainted mons.
* @returns
*/
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.
* 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 pokemon the player pokemon to apply the hp change to
* @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) {
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);

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 trigIndex
* @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 trigIndex
* @param transformationBaseBg

View File

@ -227,6 +227,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
if (!legalPlayerPokemon.length) {
this.scene.unshiftPhase(new GameOverPhase(this.scene));
return this.end();
}
// 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 { BerryModifier } from "#app/modifier/modifier";
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 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);
// Clear out any pesky mods that slipped through test spin-up
scene.modifiers.forEach(mod => {

View File

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

View File

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

View File

@ -104,7 +104,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.dexProgressContainer.setVisible(true);
this.displayEncounterOptions(slideInDescription);
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
this.setCursor(cursor);
} 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.
* Method will do pattern match/replace and apply BBCode color/shadow styling to substrings within the content:
* @[<TextStyle>]{<text to color>}
@ -236,8 +236,8 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui
* - "blue text" with TextStyle.SUMMARY_BLUE applied
* - " primaryStyle text " with primaryStyle TextStyle applied
* - "red text" with TextStyle.SUMMARY_RED applied
* @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 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 uiTheme
*/
export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string {