Merge pull request #4210 from ben-lear/mystery-encounters
PR feedback from NightKev
This commit is contained in:
commit
1870926f3b
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue