[P All][Bug] Various ME bugfixes (copy) (#4695)

* Mystery Encounter bugfixes

* more ME bug fixes

* update allowed pokemon in ME requirements

* some unit test cleanup and general tidying

* fix null exception on isBattleMysteryEncounter

* clean up tsdocs and fix pokemon hasAbility check

* fix double battle crash in challenge mode with a single eligible pokemon

* apply suggestions from PR#4619's code reviews

* revert fix for Keldeo crashes + implement fix suggestion from PR #4619

* fix session migration for PokemonCustomData

* prevent test failure due to keldeo fix

---------

Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
This commit is contained in:
MokaStitcher 2024-10-20 23:55:07 +02:00 committed by GitHub
parent 39abac65be
commit e6c06d57be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 263 additions and 134 deletions

View File

@ -3416,12 +3416,12 @@
"rotated": false, "rotated": false,
"trimmed": true, "trimmed": true,
"sourceSize": { "sourceSize": {
"w": 24, "w": 32,
"h": 24 "h": 32
}, },
"spriteSourceSize": { "spriteSourceSize": {
"x": 1, "x": 5,
"y": 2, "y": 7,
"w": 22, "w": 22,
"h": 19 "h": 19
}, },
@ -8415,6 +8415,6 @@
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.codeandweb.com/texturepacker",
"version": "3.0", "version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:934ea4080bad980d4fea720cc771f133:ed564bc47b79b15a763de57045178e88:110e074689c9edd2c54833ce2e4d9270$" "smartupdate": "$TexturePacker:SmartUpdate:9ef21166268f7487fc9ff8d0f9b996e4:82658ac7bdd4c2b417e1f59168179262:110e074689c9edd2c54833ce2e4d9270$"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 285 B

View File

@ -86,7 +86,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph
import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome } from "#app/data/mystery-encounters/mystery-encounters";
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -1207,12 +1207,10 @@ export default class BattleScene extends SceneBase {
// Check for mystery encounter // Check for mystery encounter
// Can only occur in place of a standard (non-boss) wild battle, waves 10-180 // Can only occur in place of a standard (non-boss) wild battle, waves 10-180
if (this.isWaveMysteryEncounter(newBattleType, newWaveIndex, mysteryEncounterType) || newBattleType === BattleType.MYSTERY_ENCOUNTER) { if (this.isWaveMysteryEncounter(newBattleType, newWaveIndex) || newBattleType === BattleType.MYSTERY_ENCOUNTER) {
newBattleType = BattleType.MYSTERY_ENCOUNTER; newBattleType = BattleType.MYSTERY_ENCOUNTER;
// Reset base spawn weight // Reset to base spawn weight
this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
} else if (newBattleType === BattleType.WILD) {
this.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS;
} }
} }
@ -2364,6 +2362,19 @@ export default class BattleScene extends SceneBase {
return false; return false;
} }
/**
* Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found.
* @param phaseFilter filter function
*/
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean {
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueuePrepend.splice(phaseIndex, 1);
return true;
}
return false;
}
/** /**
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase() * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
* @param phase {@linkcode Phase} the phase to be added * @param phase {@linkcode Phase} the phase to be added
@ -3125,18 +3136,26 @@ export default class BattleScene extends SceneBase {
} }
} }
/**
* Returns if a wave COULD spawn a {@linkcode MysteryEncounter}.
* Even if returns `true`, does not guarantee that a wave will actually be a ME.
* That check is made in {@linkcode BattleScene.isWaveMysteryEncounter} instead.
*/
isMysteryEncounterValidForWave(battleType: BattleType, waveIndex: number): boolean {
const [ lowestMysteryEncounterWave, highestMysteryEncounterWave ] = this.gameMode.getMysteryEncounterLegalWaves();
return this.gameMode.hasMysteryEncounters && battleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave;
}
/** /**
* Determines whether a wave should randomly generate a {@linkcode MysteryEncounter}. * Determines whether a wave should randomly generate a {@linkcode MysteryEncounter}.
* Currently, the only modes that MEs are allowed in are Classic and Challenge. * Currently, the only modes that MEs are allowed in are Classic and Challenge.
* Additionally, MEs cannot spawn outside of waves 10-180 in those modes * Additionally, MEs cannot spawn outside of waves 10-180 in those modes
*
* @param newBattleType * @param newBattleType
* @param waveIndex * @param waveIndex
* @param sessionDataEncounterType
*/ */
private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number, sessionDataEncounterType?: MysteryEncounterType): boolean { private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number): boolean {
const [ lowestMysteryEncounterWave, highestMysteryEncounterWave ] = this.gameMode.getMysteryEncounterLegalWaves(); const [ lowestMysteryEncounterWave, highestMysteryEncounterWave ] = this.gameMode.getMysteryEncounterLegalWaves();
if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave) { if (this.isMysteryEncounterValidForWave(newBattleType, waveIndex)) {
// Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor // Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor
const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance; const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance;
const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents; const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents;

View File

@ -1,18 +1,20 @@
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { Nature } from "#enums/nature";
/** /**
* Data that can customize a Pokemon in non-standard ways from its Species * Data that can customize a Pokemon in non-standard ways from its Species
* Currently only used by Mystery Encounters, may need to be renamed if it becomes more widely used * Currently only used by Mystery Encounters and Mints.
*/ */
export class MysteryEncounterPokemonData { export class CustomPokemonData {
public spriteScale: number; public spriteScale: number;
public ability: Abilities | -1; public ability: Abilities | -1;
public passive: Abilities | -1; public passive: Abilities | -1;
public nature: Nature | -1;
public types: Type[]; public types: Type[];
constructor(data?: MysteryEncounterPokemonData | Partial<MysteryEncounterPokemonData>) { constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
if (!isNullOrUndefined(data)) { if (!isNullOrUndefined(data)) {
Object.assign(this, data); Object.assign(this, data);
} }
@ -20,6 +22,7 @@ export class MysteryEncounterPokemonData {
this.spriteScale = this.spriteScale ?? -1; this.spriteScale = this.spriteScale ?? -1;
this.ability = this.ability ?? -1; this.ability = this.ability ?? -1;
this.passive = this.passive ?? -1; this.passive = this.passive ?? -1;
this.nature = this.nature ?? -1;
this.types = this.types ?? []; this.types = this.types ?? [];
} }
} }

View File

@ -133,8 +133,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
new MoveRequirement(EXTORTION_MOVES), new MoveRequirement(EXTORTION_MOVES, true),
new AbilityRequirement(EXTORTION_ABILITIES)) new AbilityRequirement(EXTORTION_ABILITIES, true))
) )
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,

View File

@ -42,6 +42,8 @@ import {
AttackTypeBoosterModifier, AttackTypeBoosterModifier,
BypassSpeedChanceModifier, BypassSpeedChanceModifier,
ContactHeldItemTransferChanceModifier, ContactHeldItemTransferChanceModifier,
GigantamaxAccessModifier,
MegaEvolutionAccessModifier,
PokemonHeldItemModifier PokemonHeldItemModifier
} from "#app/modifier/modifier"; } from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
@ -356,10 +358,17 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
}, },
]; ];
} else { } else {
// If player has any evolution/form change items that are valid for their party, will spawn one of those items in addition to a Master Ball // If the player has any evolution/form change items that are valid for their party,
const modifierOptions: ModifierTypeOption[] = [ generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)!, generateModifierTypeOption(scene, modifierTypes.MAX_LURE)! ]; // spawn one of those items in addition to Dynamax Band, Mega Band, and Master Ball
const modifierOptions: ModifierTypeOption[] = [ generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)! ];
const specialOptions: ModifierTypeOption[] = []; const specialOptions: ModifierTypeOption[] = [];
if (!scene.findModifier(m => m instanceof MegaEvolutionAccessModifier)) {
modifierOptions.push(generateModifierTypeOption(scene, modifierTypes.MEGA_BRACELET)!);
}
if (!scene.findModifier(m => m instanceof GigantamaxAccessModifier)) {
modifierOptions.push(generateModifierTypeOption(scene, modifierTypes.DYNAMAX_BAND)!);
}
const nonRareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.EVOLUTION_ITEM); const nonRareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.EVOLUTION_ITEM);
if (nonRareEvolutionModifier) { if (nonRareEvolutionModifier) {
specialOptions.push(nonRareEvolutionModifier); specialOptions.push(nonRareEvolutionModifier);

View File

@ -28,7 +28,7 @@ import { BattlerIndex } from "#app/battle";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#app/data/move"; import { MoveCategory } from "#app/data/move";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
@ -133,7 +133,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
}, },
{ // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter { // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter
species: getPokemonSpecies(Species.BLACEPHALON), species: getPokemonSpecies(Species.BLACEPHALON),
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ ability: ability, types: [ randSeedInt(18), randSeedInt(18) ]}), customPokemonData: new CustomPokemonData({ ability: ability, types: [ randSeedInt(18), randSeedInt(18) ]}),
isBoss: true, isBoss: true,
moveSet: [ Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN ] moveSet: [ Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN ]
}, },
@ -353,15 +353,15 @@ export const ClowningAroundEncounter: MysteryEncounter =
newTypes.push(secondType); newTypes.push(secondType);
// Apply the type changes (to both base and fusion, if pokemon is fused) // Apply the type changes (to both base and fusion, if pokemon is fused)
if (!pokemon.mysteryEncounterPokemonData) { if (!pokemon.customPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); pokemon.customPokemonData = new CustomPokemonData();
} }
pokemon.mysteryEncounterPokemonData.types = newTypes; pokemon.customPokemonData.types = newTypes;
if (pokemon.isFusion()) { if (pokemon.isFusion()) {
if (!pokemon.fusionMysteryEncounterPokemonData) { if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData(); pokemon.fusionCustomPokemonData = new CustomPokemonData();
} }
pokemon.fusionMysteryEncounterPokemonData.types = newTypes; pokemon.fusionCustomPokemonData.types = newTypes;
} }
} }
}) })
@ -426,15 +426,15 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
// Do ability swap // Do ability swap
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
if (pokemon.isFusion()) { if (pokemon.isFusion()) {
if (!pokemon.fusionMysteryEncounterPokemonData) { if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData(); pokemon.fusionCustomPokemonData = new CustomPokemonData();
} }
pokemon.fusionMysteryEncounterPokemonData.ability = encounter.misc.ability; pokemon.fusionCustomPokemonData.ability = encounter.misc.ability;
} else { } else {
if (!pokemon.mysteryEncounterPokemonData) { if (!pokemon.customPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); pokemon.customPokemonData = new CustomPokemonData();
} }
pokemon.mysteryEncounterPokemonData.ability = encounter.misc.ability; pokemon.customPokemonData.ability = encounter.misc.ability;
} }
encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));

View File

@ -236,7 +236,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically .withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES, true)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -145,7 +145,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES, true)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`, buttonTooltip: `${namespace}:option.2.tooltip`,

View File

@ -227,7 +227,7 @@ export const PartTimerEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically .withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES, true)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -138,7 +138,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
newNature = randSeedInt(25) as Nature; newNature = randSeedInt(25) as Nature;
} }
chosenPokemon.nature = newNature; chosenPokemon.customPokemonData.nature = newNature;
encounter.setDialogueToken("newNature", getNatureName(newNature)); encounter.setDialogueToken("newNature", getNatureName(newNature));
queueEncounterMessage(scene, `${namespace}:cheap_side_effects`); queueEncounterMessage(scene, `${namespace}:cheap_side_effects`);
setEncounterExp(scene, [ chosenPokemon.id ], 100); setEncounterExp(scene, [ chosenPokemon.id ], 100);

View File

@ -18,7 +18,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
/** i18n namespace for the encounter */ /** i18n namespace for the encounter */
const namespace = "mysteryEncounters/slumberingSnorlax"; const namespace = "mysteryEncounters/slumberingSnorlax";
@ -72,7 +72,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
stackCount: 2 stackCount: 2
}, },
], ],
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep
}; };
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -143,7 +143,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES, true))
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -25,6 +25,7 @@ import { achvs } from "#app/system/achv";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { getPokeballTintColor } from "#app/data/pokeball"; import { getPokeballTintColor } from "#app/data/pokeball";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theExpertPokemonBreeder"; const namespace = "mysteryEncounters/theExpertPokemonBreeder";
@ -163,7 +164,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
if (pokemon2CommonEggs > 0) { if (pokemon2CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") }); const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { eggs: eggsText }); pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText); encounter.setDialogueToken("pokemon2CommonEggs", eggsText);
} }
encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip; encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip;
@ -221,7 +222,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon1; encounter.misc.chosenPokemon = pokemon1;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
@ -247,9 +248,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build() .build()
) )
.withOption( .withOption(
@ -273,7 +271,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon2; encounter.misc.chosenPokemon = pokemon2;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
@ -299,9 +297,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build() .build()
) )
.withOption( .withOption(
@ -325,7 +320,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon3; encounter.misc.chosenPokemon = pokemon3;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
@ -351,9 +346,6 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene);
})
.build() .build()
) )
.withOutroDialogue([ .withOutroDialogue([
@ -521,19 +513,19 @@ function checkAchievement(scene: BattleScene) {
} }
} }
async function restorePartyAndHeldItems(scene: BattleScene) { function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party // Restore original party
scene.getParty().push(...encounter.misc.originalParty); scene.getParty().push(...encounter.misc.originalParty);
// Restore held items // Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems; const originalHeldItems = encounter.misc.originalPartyHeldItems;
originalHeldItems.forEach(pokemonHeldItemsList => { originalHeldItems.forEach((pokemonHeldItemsList: PokemonHeldItemModifier[]) => {
pokemonHeldItemsList.forEach(heldItem => { pokemonHeldItemsList.forEach(heldItem => {
scene.addModifier(heldItem, true, false, false, true); scene.addModifier(heldItem, true, false, false, true);
}); });
}); });
await scene.updateModifiers(true); scene.updateModifiers(true);
} }
function onGameOver(scene: BattleScene) { function onGameOver(scene: BattleScene) {
@ -609,13 +601,13 @@ function onGameOver(scene: BattleScene) {
return false; return false;
} }
async function doPostEncounterCleanup(scene: BattleScene) { function doPostEncounterCleanup(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
if (!encounter.misc.encounterFailed) { if (!encounter.misc.encounterFailed) {
// Give achievement if in Space biome // Give achievement if in Space biome
checkAchievement(scene); checkAchievement(scene);
// Give 20 friendship to the chosen pokemon // Give 20 friendship to the chosen pokemon
encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED); encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene); restorePartyAndHeldItems(scene);
} }
} }

View File

@ -14,7 +14,7 @@ import { BattlerIndex } from "#app/battle";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
@ -79,7 +79,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE), species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true, isBoss: true,
bossSegments: 5, bossSegments: 5,
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ], moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],
modifierConfigs: [ modifierConfigs: [

View File

@ -210,7 +210,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically .withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES, true)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -12,7 +12,7 @@ import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
@ -379,10 +379,10 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
newType = randSeedInt(18) as Type; newType = randSeedInt(18) as Type;
} }
newTypes.push(newType); newTypes.push(newType);
if (!newPokemon.mysteryEncounterPokemonData) { if (!newPokemon.customPokemonData) {
newPokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); newPokemon.customPokemonData = new CustomPokemonData();
} }
newPokemon.mysteryEncounterPokemonData.types = newTypes; newPokemon.customPokemonData.types = newTypes;
for (const item of transformation.heldItems) { for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id; item.pokemonId = newPokemon.id;

View File

@ -15,6 +15,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { allAbilities } from "#app/data/ability";
export interface EncounterRequirement { export interface EncounterRequirement {
meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met
@ -476,9 +477,11 @@ export class MoveRequirement extends EncounterPokemonRequirement {
requiredMoves: Moves[] = []; requiredMoves: Moves[] = [];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
excludeDisallowedPokemon: boolean;
constructor(moves: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { constructor(moves: Moves | Moves[], excludeDisallowedPokemon: boolean, minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
super(); super();
this.excludeDisallowedPokemon = excludeDisallowedPokemon;
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredMoves = Array.isArray(moves) ? moves : [ moves ]; this.requiredMoves = Array.isArray(moves) ? moves : [ moves ];
@ -494,10 +497,15 @@ export class MoveRequirement extends EncounterPokemonRequirement {
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length > 0).length > 0); // get the Pokemon with at least one move in the required moves list
return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& pokemon.moveset.some((move) => move?.moveId && this.requiredMoves.includes(move.moveId)));
} else { } else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves // for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length === 0).length === 0); return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& !pokemon.moveset.some((move) => move?.moveId && this.requiredMoves.includes(move.moveId)));
} }
} }
@ -559,9 +567,11 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
requiredAbilities: Abilities[]; requiredAbilities: Abilities[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
excludeDisallowedPokemon: boolean;
constructor(abilities: Abilities | Abilities[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { constructor(abilities: Abilities | Abilities[], excludeDisallowedPokemon: boolean, minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
super(); super();
this.excludeDisallowedPokemon = excludeDisallowedPokemon;
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredAbilities = Array.isArray(abilities) ? abilities : [ abilities ]; this.requiredAbilities = Array.isArray(abilities) ? abilities : [ abilities ];
@ -577,16 +587,21 @@ export class AbilityRequirement extends EncounterPokemonRequirement {
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredAbilities.some((ability) => pokemon.getAbility().id === ability)); return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& this.requiredAbilities.some((ability) => pokemon.hasAbility(ability, false)));
} else { } else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess // for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilities
return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((ability) => pokemon.getAbility().id === ability).length === 0); return partyPokemon.filter((pokemon) =>
(!this.excludeDisallowedPokemon || pokemon.isAllowedInBattle())
&& this.requiredAbilities.filter((ability) => pokemon.hasAbility(ability, false)).length === 0);
} }
} }
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(_scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
if (pokemon?.getAbility().id && this.requiredAbilities.some(a => pokemon.getAbility().id === a)) { const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false));
return [ "ability", pokemon.getAbility().name ]; if (!isNullOrUndefined(matchingAbility)) {
return [ "ability", allAbilities[matchingAbility].name ];
} }
return [ "ability", "" ]; return [ "ability", "" ];
} }

View File

@ -325,7 +325,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
if (activeMon.length > 0) { if (activeMon.length > 0) {
this.primaryPokemon = activeMon[0]; this.primaryPokemon = activeMon[0];
} else { } else {
this.primaryPokemon = scene.getParty().filter(p => !p.isFainted())[0]; this.primaryPokemon = scene.getParty().filter(p => p.isAllowedInBattle())[0];
} }
return true; return true;
} }

View File

@ -27,7 +27,7 @@ import { Status, StatusEffect } from "#app/data/status-effect";
import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config";
import PokemonSpecies from "#app/data/pokemon-species"; import PokemonSpecies from "#app/data/pokemon-species";
import { Egg, IEggOptions } from "#app/data/egg"; import { Egg, IEggOptions } from "#app/data/egg";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import HeldModifierConfig from "#app/interfaces/held-modifier-config"; import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
@ -71,7 +71,7 @@ export interface EnemyPokemonConfig {
nickname?: string; nickname?: string;
bossSegments?: number; bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number bossSegmentModifier?: number; // Additive to the determined segment number
mysteryEncounterPokemonData?: MysteryEncounterPokemonData; customPokemonData?: CustomPokemonData;
formIndex?: number; formIndex?: number;
abilityIndex?: number; abilityIndex?: number;
level?: number; level?: number;
@ -145,7 +145,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
newTrainer.setVisible(false); newTrainer.setVisible(false);
scene.field.add(newTrainer); scene.field.add(newTrainer);
scene.currentBattle.trainer = newTrainer; scene.currentBattle.trainer = newTrainer;
loadEnemyAssets.push(newTrainer.loadAssets()); loadEnemyAssets.push(newTrainer.loadAssets().then(() => newTrainer.initSprite()));
battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex); battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex);
} else { } else {
@ -250,8 +250,8 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
} }
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) { if (!isNullOrUndefined(config.customPokemonData)) {
enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData; enemyPokemon.customPokemonData = config.customPokemonData;
} }
// Set Boss // Set Boss

View File

@ -62,7 +62,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates";
@ -114,7 +114,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public fusionVariant: Variant; public fusionVariant: Variant;
public fusionGender: Gender; public fusionGender: Gender;
public fusionLuck: integer; public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData | null; public fusionCustomPokemonData: CustomPokemonData | null;
private summonDataPrimer: PokemonSummonData | null; private summonDataPrimer: PokemonSummonData | null;
@ -122,7 +122,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public battleData: PokemonBattleData; public battleData: PokemonBattleData;
public battleSummonData: PokemonBattleSummonData; public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData; public turnData: PokemonTurnData;
public mysteryEncounterPokemonData: MysteryEncounterPokemonData; public customPokemonData: CustomPokemonData;
/** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */
public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -193,7 +193,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
this.nature = dataSource.nature || 0 as Nature; this.nature = dataSource.nature || 0 as Nature;
this.nickname = dataSource.nickname; this.nickname = dataSource.nickname;
this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1;
this.moveset = dataSource.moveset; this.moveset = dataSource.moveset;
this.status = dataSource.status!; // TODO: is this bang correct? this.status = dataSource.status!; // TODO: is this bang correct?
this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship; this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship;
@ -212,9 +211,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = dataSource.fusionVariant || 0; this.fusionVariant = dataSource.fusionVariant || 0;
this.fusionGender = dataSource.fusionGender; this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck; this.fusionLuck = dataSource.fusionLuck;
this.fusionMysteryEncounterPokemonData = dataSource.fusionMysteryEncounterPokemonData; this.fusionCustomPokemonData = dataSource.fusionCustomPokemonData;
this.usedTMs = dataSource.usedTMs ?? []; this.usedTMs = dataSource.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData); this.customPokemonData = new CustomPokemonData(dataSource.customPokemonData);
} else { } else {
this.id = Utils.randSeedInt(4294967296); this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id); this.ivs = ivs || Utils.getIvsFromId(this.id);
@ -235,7 +234,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.variant = this.shiny ? this.generateVariant() : 0; this.variant = this.shiny ? this.generateVariant() : 0;
} }
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); this.customPokemonData = new CustomPokemonData();
if (nature !== undefined) { if (nature !== undefined) {
this.setNature(nature); this.setNature(nature);
@ -243,8 +242,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.generateNature(); this.generateNature();
} }
this.natureOverride = -1;
this.friendship = species.baseFriendship; this.friendship = species.baseFriendship;
this.metLevel = level; this.metLevel = level;
this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1; this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1;
@ -593,8 +590,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const formKey = this.getFormKey(); const formKey = this.getFormKey();
if (this.isMax() === true || formKey === "segin-starmobile" || formKey === "schedar-starmobile" || formKey === "navi-starmobile" || formKey === "ruchbah-starmobile" || formKey === "caph-starmobile") { if (this.isMax() === true || formKey === "segin-starmobile" || formKey === "schedar-starmobile" || formKey === "navi-starmobile" || formKey === "ruchbah-starmobile" || formKey === "caph-starmobile") {
return 1.5; return 1.5;
} else if (this.mysteryEncounterPokemonData.spriteScale > 0) { } else if (this.customPokemonData.spriteScale > 0) {
return this.mysteryEncounterPokemonData.spriteScale; return this.customPokemonData.spriteScale;
} }
return 1; return 1;
} }
@ -1023,7 +1020,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
getNature(): Nature { getNature(): Nature {
return this.natureOverride !== -1 ? this.natureOverride : this.nature; return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature;
} }
setNature(nature: Nature): void { setNature(nature: Nature): void {
@ -1198,15 +1195,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!types.length || !includeTeraType) { if (!types.length || !includeTeraType) {
if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) { if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) {
this.summonData.types.forEach(t => types.push(t)); this.summonData.types.forEach(t => types.push(t));
} else if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) { } else if (this.customPokemonData.types && this.customPokemonData.types.length > 0) {
// "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters // "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters
types.push(this.mysteryEncounterPokemonData.types[0]); types.push(this.customPokemonData.types[0]);
// Fusing a Pokemon onto something with "permanently changed" types will still apply the fusion's types as normal // Fusing a Pokemon onto something with "permanently changed" types will still apply the fusion's types as normal
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
if (fusionSpeciesForm) { if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types // Check if the fusion Pokemon also had "permanently changed" types
const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types; const fusionMETypes = this.fusionCustomPokemonData?.types;
if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) { if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]); types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) { } else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
@ -1218,8 +1215,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (types.length === 1 && this.mysteryEncounterPokemonData.types.length >= 2) { if (types.length === 1 && this.customPokemonData.types.length >= 2) {
types.push(this.mysteryEncounterPokemonData.types[1]); types.push(this.customPokemonData.types[1]);
} }
} else { } else {
const speciesForm = this.getSpeciesForm(ignoreOverride); const speciesForm = this.getSpeciesForm(ignoreOverride);
@ -1230,7 +1227,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (fusionSpeciesForm) { if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types // Check if the fusion Pokemon also had "permanently changed" types
// Otherwise, use standard fusion type logic // Otherwise, use standard fusion type logic
const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types; const fusionMETypes = this.fusionCustomPokemonData?.types;
if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) { if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]); types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) { } else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
@ -1262,6 +1259,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
// If both types are the same (can happen in weird custom typing scenarios), reduce to single type
if (types.length > 1 && types[0] === types[1]) {
types.splice(0, 1);
}
return types; return types;
} }
@ -1288,14 +1290,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
} }
if (this.isFusion()) { if (this.isFusion()) {
if (!isNullOrUndefined(this.fusionMysteryEncounterPokemonData?.ability) && this.fusionMysteryEncounterPokemonData.ability !== -1) { if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
return allAbilities[this.fusionMysteryEncounterPokemonData.ability]; return allAbilities[this.fusionCustomPokemonData.ability];
} else { } else {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
} }
} }
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) { if (!isNullOrUndefined(this.customPokemonData.ability) && this.customPokemonData.ability !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.ability]; return allAbilities[this.customPokemonData.ability];
} }
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
if (abilityId === Abilities.NONE) { if (abilityId === Abilities.NONE) {
@ -1318,8 +1320,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
} }
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.passive) && this.mysteryEncounterPokemonData.passive !== -1) { if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.passive]; return allAbilities[this.customPokemonData.passive];
} }
let starterSpeciesId = this.species.speciesId; let starterSpeciesId = this.species.speciesId;
@ -2018,7 +2020,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = 0; this.fusionVariant = 0;
this.fusionGender = 0; this.fusionGender = 0;
this.fusionLuck = 0; this.fusionLuck = 0;
this.fusionMysteryEncounterPokemonData = null; this.fusionCustomPokemonData = null;
this.generateName(); this.generateName();
this.calculateStats(); this.calculateStats();
@ -2187,8 +2189,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0)); this.moveset.push(new PokemonMove(movePool[index][0], 0, 0));
} }
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
if (this.isPlayer() || !this.scene.currentBattle?.isBattleMysteryEncounter() || !this.scene.currentBattle?.mysteryEncounter) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);
} }
}
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
const move = this.getMoveset().length > moveIndex const move = this.getMoveset().length > moveIndex
@ -4292,12 +4297,33 @@ export class PlayerPokemon extends Pokemon {
changeForm(formChange: SpeciesFormChange): Promise<void> { changeForm(formChange: SpeciesFormChange): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const previousFormIndex = this.formIndex;
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0); this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
this.generateName(); this.generateName();
const abilityCount = this.getSpeciesForm().getAbilityCount(); const abilityCount = this.getSpeciesForm().getAbilityCount();
if (this.abilityIndex >= abilityCount) { // Shouldn't happen if (this.abilityIndex >= abilityCount) { // Shouldn't happen
this.abilityIndex = abilityCount - 1; this.abilityIndex = abilityCount - 1;
} }
// In cases where a form change updates the type of a Pokemon from its previous form (Arceus, Silvally, Castform, etc.),
// persist that type change in customPokemonData if necessary
const baseForm = this.species.forms[previousFormIndex];
const baseFormTypes = [ baseForm.type1, baseForm.type2 ];
if (this.customPokemonData.types.length > 0) {
if (this.getSpeciesForm().type1 !== baseFormTypes[0]) {
this.customPokemonData.types[0] = this.getSpeciesForm().type1;
}
const type2 = this.getSpeciesForm().type2;
if (!isNullOrUndefined(type2) && type2 !== baseFormTypes[1]) {
if (this.customPokemonData.types.length > 1) {
this.customPokemonData.types[1] = type2;
} else {
this.customPokemonData.types.push(type2);
}
}
}
this.compatibleTms.splice(0, this.compatibleTms.length); this.compatibleTms.splice(0, this.compatibleTms.length);
this.generateCompatibleTms(); this.generateCompatibleTms();
const updateAndResolve = () => { const updateAndResolve = () => {
@ -4334,7 +4360,7 @@ export class PlayerPokemon extends Pokemon {
this.fusionVariant = pokemon.variant; this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender; this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck; this.fusionLuck = pokemon.luck;
this.fusionMysteryEncounterPokemonData = pokemon.mysteryEncounterPokemonData; this.fusionCustomPokemonData = pokemon.customPokemonData;
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) { if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) {
this.pauseEvolutions = true; this.pauseEvolutions = true;
} }

View File

@ -1126,7 +1126,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
} }
class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator { class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
constructor(rare: boolean) { constructor(isRareFormChangeItem: boolean) {
super((party: Pokemon[], pregenArgs?: any[]) => { super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in FormChangeItem)) { if (pregenArgs && (pregenArgs.length === 1) && (pregenArgs[0] in FormChangeItem)) {
return new FormChangeItemModifierType(pregenArgs[0] as FormChangeItem); return new FormChangeItemModifierType(pregenArgs[0] as FormChangeItem);
@ -1167,7 +1167,7 @@ class FormChangeItemModifierTypeGenerator extends ModifierTypeGenerator {
} }
return formChangeItemTriggers; return formChangeItemTriggers;
}).flat()) }).flat())
].flat().flatMap(fc => fc.item).filter(i => (i && i < 100) === rare); ].flat().flatMap(fc => fc.item).filter(i => (i && i < 100) === isRareFormChangeItem);
// convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party. // convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party.
if (!formChangeItemPool.length) { if (!formChangeItemPool.length) {

View File

@ -2181,7 +2181,7 @@ export class PokemonNatureChangeModifier extends ConsumablePokemonModifier {
* @returns * @returns
*/ */
override apply(playerPokemon: PlayerPokemon): boolean { override apply(playerPokemon: PlayerPokemon): boolean {
playerPokemon.natureOverride = this.nature; playerPokemon.customPokemonData.nature = this.nature;
let speciesId = playerPokemon.species.speciesId; let speciesId = playerPokemon.species.speciesId;
playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1); playerPokemon.scene.gameData.dexData[speciesId].natureAttr |= 1 << (this.nature + 1);

View File

@ -35,6 +35,7 @@ import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-d
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
export class EncounterPhase extends BattlePhase { export class EncounterPhase extends BattlePhase {
private loaded: boolean; private loaded: boolean;
@ -68,7 +69,7 @@ export class EncounterPhase extends BattlePhase {
this.scene.executeWithSeedOffset(() => { this.scene.executeWithSeedOffset(() => {
const currentSessionEncounterType = battle.mysteryEncounterType; const currentSessionEncounterType = battle.mysteryEncounterType;
battle.mysteryEncounter = this.scene.getMysteryEncounter(currentSessionEncounterType); battle.mysteryEncounter = this.scene.getMysteryEncounter(currentSessionEncounterType);
}, battle.waveIndex << 4); }, battle.waveIndex * 16);
} }
const mysteryEncounter = battle.mysteryEncounter; const mysteryEncounter = battle.mysteryEncounter;
if (mysteryEncounter) { if (mysteryEncounter) {
@ -251,6 +252,13 @@ export class EncounterPhase extends BattlePhase {
this.scene.updateModifiers(true); this.scene.updateModifiers(true);
}*/ }*/
const { battleType, waveIndex } = this.scene.currentBattle;
if (this.scene.isMysteryEncounterValidForWave(battleType, waveIndex) && !this.scene.currentBattle.isBattleMysteryEncounter()) {
// Increment ME spawn chance if an ME could have spawned but did not
// Only do this AFTER session has been saved to avoid duplicating increments
this.scene.mysteryEncounterSaveData.encounterSpawnChance += WEIGHT_INCREMENT_ON_SPAWN_MISS;
}
for (const pokemon of this.scene.getParty()) { for (const pokemon of this.scene.getParty()) {
if (pokemon) { if (pokemon) {
pokemon.resetBattleData(); pokemon.resetBattleData();

View File

@ -402,7 +402,7 @@ export class MysteryEncounterBattlePhase extends Phase {
} }
} }
const availablePartyMembers = scene.getParty().filter(p => !p.isFainted()); const availablePartyMembers = scene.getParty().filter(p => p.isAllowedInBattle());
if (!availablePartyMembers[0].isOnField()) { if (!availablePartyMembers[0].isOnField()) {
scene.pushPhase(new SummonPhase(scene, 0)); scene.pushPhase(new SummonPhase(scene, 0));

View File

@ -12,7 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
export default class PokemonData { export default class PokemonData {
public id: integer; public id: integer;
@ -33,7 +33,6 @@ export default class PokemonData {
public stats: integer[]; public stats: integer[];
public ivs: integer[]; public ivs: integer[];
public nature: Nature; public nature: Nature;
public natureOverride: Nature | -1;
public moveset: (PokemonMove | null)[]; public moveset: (PokemonMove | null)[];
public status: Status | null; public status: Status | null;
public friendship: integer; public friendship: integer;
@ -54,14 +53,20 @@ export default class PokemonData {
public fusionVariant: Variant; public fusionVariant: Variant;
public fusionGender: Gender; public fusionGender: Gender;
public fusionLuck: integer; public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData;
public boss: boolean; public boss: boolean;
public bossSegments?: integer; public bossSegments?: integer;
public summonData: PokemonSummonData; public summonData: PokemonSummonData;
/** Data that can customize a Pokemon in non-standard ways from its Species */ /** Data that can customize a Pokemon in non-standard ways from its Species */
public mysteryEncounterPokemonData: MysteryEncounterPokemonData; public customPokemonData: CustomPokemonData;
public fusionCustomPokemonData: CustomPokemonData;
// Deprecated attributes, needed for now to allow SessionData migration (see PR#4619 comments)
public natureOverride: Nature | -1;
public mysteryEncounterPokemonData: CustomPokemonData | null;
public fusionMysteryEncounterPokemonData: CustomPokemonData | null;
constructor(source: Pokemon | any, forHistory: boolean = false) { constructor(source: Pokemon | any, forHistory: boolean = false) {
const sourcePokemon = source instanceof Pokemon ? source : null; const sourcePokemon = source instanceof Pokemon ? source : null;
@ -107,9 +112,13 @@ export default class PokemonData {
this.fusionVariant = source.fusionVariant; this.fusionVariant = source.fusionVariant;
this.fusionGender = source.fusionGender; this.fusionGender = source.fusionGender;
this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0); this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0);
this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData);
this.usedTMs = source.usedTMs ?? []; this.usedTMs = source.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(source.mysteryEncounterPokemonData); this.customPokemonData = new CustomPokemonData(source.customPokemonData);
this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData);
this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData);
if (!forHistory) { if (!forHistory) {
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss); this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);

View File

@ -4,6 +4,9 @@ import { version } from "../../../package.json";
// --- v1.0.4 (and below) PATCHES --- // // --- v1.0.4 (and below) PATCHES --- //
import * as v1_0_4 from "./versions/v1_0_4"; import * as v1_0_4 from "./versions/v1_0_4";
// --- v1.1.0 PATCHES --- //
import * as v1_1_0 from "./versions/v1_1_0";
const LATEST_VERSION = version.split(".").map(value => parseInt(value)); const LATEST_VERSION = version.split(".").map(value => parseInt(value));
/** /**
@ -131,6 +134,10 @@ class SessionVersionConverter extends VersionConverter {
this.callMigrators(data, v1_0_4.sessionMigrators); this.callMigrators(data, v1_0_4.sessionMigrators);
} }
} }
if (curMinor <= 1) {
console.log("Applying v1.1.0 session data migration!");
this.callMigrators(data, v1_1_0.sessionMigrators);
}
} }
console.log(`Session data successfully migrated to v${version}!`); console.log(`Session data successfully migrated to v${version}!`);
@ -153,6 +160,10 @@ class SystemVersionConverter extends VersionConverter {
this.callMigrators(data, v1_0_4.systemMigrators); this.callMigrators(data, v1_0_4.systemMigrators);
} }
} }
if (curMinor <= 1) {
console.log("Applying v1.1.0 system data migraton!");
this.callMigrators(data, v1_1_0.systemMigrators);
}
} }
console.log(`System data successfully migrated to v${version}!`); console.log(`System data successfully migrated to v${version}!`);
@ -175,6 +186,10 @@ class SettingsVersionConverter extends VersionConverter {
this.callMigrators(data, v1_0_4.settingsMigrators); this.callMigrators(data, v1_0_4.settingsMigrators);
} }
} }
if (curMinor <= 1) {
console.log("Applying v1.1.0 settings data migraton!");
this.callMigrators(data, v1_1_0.settingsMigrators);
}
} }
console.log(`System data successfully migrated to v${version}!`); console.log(`System data successfully migrated to v${version}!`);

View File

@ -0,0 +1,32 @@
import { SessionSaveData } from "../../game-data";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
export const systemMigrators = [] as const;
export const settingsMigrators = [] as const;
export const sessionMigrators = [
/**
* Converts old Pokemon natureOverride and mysteryEncounterData
* to use the new conjoined {@linkcode Pokemon.customPokemonData} structure instead.
* @param data {@linkcode SessionSaveData}
*/
function migrateCustomPokemonDataAndNatureOverrides(data: SessionSaveData) {
// Fix Pokemon nature overrides and custom data migration
data.party.forEach(pokemon => {
if (pokemon["mysteryEncounterPokemonData"]) {
pokemon.customPokemonData = new CustomPokemonData(pokemon["mysteryEncounterPokemonData"]);
pokemon["mysteryEncounterPokemonData"] = null;
}
if (pokemon["fusionMysteryEncounterPokemonData"]) {
pokemon.fusionCustomPokemonData = new CustomPokemonData(pokemon["fusionMysteryEncounterPokemonData"]);
pokemon["fusionMysteryEncounterPokemonData"] = null;
}
pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData();
if (pokemon["natureOverride"] && pokemon["natureOverride"] >= 0) {
pokemon.customPokemonData.nature = pokemon["natureOverride"];
pokemon["natureOverride"] = -1;
}
});
}
] as const;

View File

@ -476,10 +476,11 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(3); expect(modifierSelectHandler.options.length).toEqual(4);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MASTER_BALL"); expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MASTER_BALL");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MEGA_BRACELET");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM"); expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("DYNAMAX_BAND");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM");
}); });
it("should leave encounter without battle", async () => { it("should leave encounter without battle", async () => {

View File

@ -118,11 +118,11 @@ describe("Clowning Around - Mystery Encounter", () => {
}); });
expect(config.pokemonConfigs?.[1]).toEqual({ expect(config.pokemonConfigs?.[1]).toEqual({
species: getPokemonSpecies(Species.BLACEPHALON), species: getPokemonSpecies(Species.BLACEPHALON),
mysteryEncounterPokemonData: expect.anything(), customPokemonData: expect.anything(),
isBoss: true, isBoss: true,
moveSet: [ Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN ] moveSet: [ Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN ]
}); });
expect(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.types.length).toBe(2); expect(config.pokemonConfigs?.[1].customPokemonData?.types.length).toBe(2);
expect([ expect([
Abilities.STURDY, Abilities.STURDY,
Abilities.PICKUP, Abilities.PICKUP,
@ -139,8 +139,8 @@ describe("Clowning Around - Mystery Encounter", () => {
Abilities.MAGICIAN, Abilities.MAGICIAN,
Abilities.SHEER_FORCE, Abilities.SHEER_FORCE,
Abilities.PRANKSTER Abilities.PRANKSTER
]).toContain(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability); ]).toContain(config.pokemonConfigs?.[1].customPokemonData?.ability);
expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability); expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].customPokemonData?.ability);
await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true); expect(onInitResult).toBe(true);
@ -219,7 +219,7 @@ describe("Clowning Around - Mystery Encounter", () => {
await game.phaseInterceptor.to(NewBattlePhase, false); await game.phaseInterceptor.to(NewBattlePhase, false);
const leadPokemon = scene.getParty()[0]; const leadPokemon = scene.getParty()[0];
expect(leadPokemon.mysteryEncounterPokemonData?.ability).toBe(abilityToTrain); expect(leadPokemon.customPokemonData?.ability).toBe(abilityToTrain);
}); });
}); });
@ -340,9 +340,9 @@ describe("Clowning Around - Mystery Encounter", () => {
scene.getParty()[2].moveset = []; scene.getParty()[2].moveset = [];
await runMysteryEncounterToEnd(game, 3); await runMysteryEncounterToEnd(game, 3);
const leadTypesAfter = scene.getParty()[0].mysteryEncounterPokemonData?.types; const leadTypesAfter = scene.getParty()[0].customPokemonData?.types;
const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterPokemonData?.types; const secondaryTypesAfter = scene.getParty()[1].customPokemonData?.types;
const thirdTypesAfter = scene.getParty()[2].mysteryEncounterPokemonData?.types; const thirdTypesAfter = scene.getParty()[2].customPokemonData?.types;
expect(leadTypesAfter.length).toBe(2); expect(leadTypesAfter.length).toBe(2);
expect(leadTypesAfter[0]).toBe(Type.WATER); expect(leadTypesAfter[0]).toBe(Type.WATER);

View File

@ -21,7 +21,7 @@ import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modif
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
@ -109,7 +109,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE), species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true, isBoss: true,
bossSegments: 5, bossSegments: 5,
mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ], moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],
modifierConfigs: expect.any(Array), modifierConfigs: expect.any(Array),

View File

@ -122,7 +122,7 @@ describe("Weird Dream - Mystery Encounter", () => {
for (let i = 0; i < pokemonAfter.length; i++) { for (let i = 0; i < pokemonAfter.length; i++) {
const newPokemon = pokemonAfter[i]; const newPokemon = pokemonAfter[i];
expect(newPokemon.getSpeciesForm().speciesId).not.toBe(pokemonPrior[i].getSpeciesForm().speciesId); expect(newPokemon.getSpeciesForm().speciesId).not.toBe(pokemonPrior[i].getSpeciesForm().speciesId);
expect(newPokemon.mysteryEncounterPokemonData?.types.length).toBe(2); expect(newPokemon.customPokemonData?.types.length).toBe(2);
} }
const plus90To110 = bstDiff.filter(bst => bst > 80); const plus90To110 = bstDiff.filter(bst => bst > 80);