ME data schema updates

This commit is contained in:
ImperialSympathizer 2024-09-07 20:56:47 -04:00
parent 70a703f152
commit 0db39f9a1d
20 changed files with 204 additions and 167 deletions

View File

@ -30,7 +30,7 @@
- [ ] The PR is self-contained and cannot be split into smaller PRs?
- [ ] Have I provided a clear explanation of the changes?
- [ ] Have I considered writing automated tests for the issue?
- [ ] If I have text, did I add make it translatable and added a key in the English language?
- [ ] If I have text, did I make it translatable and add a key in the English locale file(s)?
- [ ] Have I tested the changes (manually)?
- [ ] Are all unit tests still passing? (`npm run test`)
- [ ] Are the changes visual?

View File

@ -4,7 +4,7 @@ import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon";
import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species";
import { Constructor, isNullOrUndefined } from "#app/utils";
import * as Utils from "./utils";
import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier";
import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems, PokemonIncrementingStatModifier, ExpShareModifier, ExpBalanceModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
import { Phase } from "./phase";
@ -96,10 +96,13 @@ import { TurnInitPhase } from "./phases/turn-init-phase";
import { ShopCursorTarget } from "./enums/shop-cursor-target";
import MysteryEncounter from "./data/mystery-encounters/mystery-encounter";
import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters";
import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data";
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import { ExpPhase } from "#app/phases/exp-phase";
import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -253,7 +256,7 @@ export default class BattleScene extends SceneBase {
public money: integer;
public pokemonInfoContainer: PokemonInfoContainer;
private party: PlayerPokemon[];
public mysteryEncounterData: MysteryEncounterData = new MysteryEncounterData(null);
public mysteryEncounterSaveData: MysteryEncounterSaveData = new MysteryEncounterSaveData(null);
public lastMysteryEncounter?: MysteryEncounter;
/** Combined Biome and Wave count text */
private biomeWaveText: Phaser.GameObjects.Text;
@ -1168,8 +1171,8 @@ export default class BattleScene extends SceneBase {
const roll = Utils.randSeedInt(256);
// 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.mysteryEncounterData.encounterSpawnChance;
const encounteredEvents = this.mysteryEncounterData.encounteredEvents;
const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance;
const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents;
// If total number of encounters is lower than expected for the run, slightly favor a new encounter spawn (reverse as well)
// Reduces occurrence of runs with total encounters significantly different from AVERAGE_ENCOUNTERS_PER_RUN_TARGET
@ -1185,9 +1188,9 @@ export default class BattleScene extends SceneBase {
if (canSpawn && roll < successRate) {
newBattleType = BattleType.MYSTERY_ENCOUNTER;
// Reset base spawn weight
this.mysteryEncounterData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
} else {
this.mysteryEncounterData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS;
this.mysteryEncounterSaveData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS;
}
}
}
@ -2917,6 +2920,96 @@ export default class BattleScene extends SceneBase {
this.shiftPhase();
}
applyPartyExp(expValue: number): void {
const participantIds = this.currentBattle.playerParticipantIds;
const party = this.getParty();
const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier;
const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier;
const nonFaintedPartyMembers = party.filter(p => p.hp);
const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.getMaxExpLevel());
const partyMemberExp: number[] = [];
if (participantIds.size) {
if (this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
expValue = Math.floor(expValue * 1.5);
} else if (this.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.currentBattle.mysteryEncounter) {
expValue = Math.floor(expValue * this.currentBattle.mysteryEncounter.expMultiplier);
}
for (const partyMember of nonFaintedPartyMembers) {
const pId = partyMember.id;
const participated = participantIds.has(pId);
if (participated) {
partyMember.addFriendship(2);
const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier);
if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) {
machoBraceModifier.stackCount++;
this.updateModifiers(true, true);
partyMember.updateInfo();
}
}
if (!expPartyMembers.includes(partyMember)) {
continue;
}
if (!participated && !expShareModifier) {
partyMemberExp.push(0);
continue;
}
let expMultiplier = 0;
if (participated) {
expMultiplier += (1 / participantIds.size);
if (participantIds.size > 1 && multipleParticipantExpBonusModifier) {
expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2;
}
} else if (expShareModifier) {
expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size;
}
if (partyMember.pokerus) {
expMultiplier *= 1.5;
}
if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) {
expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE;
}
const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier);
this.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp);
partyMemberExp.push(Math.floor(pokemonExp.value));
}
if (expBalanceModifier) {
let totalLevel = 0;
let totalExp = 0;
expPartyMembers.forEach((expPartyMember, epm) => {
totalExp += partyMemberExp[epm];
totalLevel += expPartyMember.level;
});
const medianLevel = Math.floor(totalLevel / expPartyMembers.length);
const recipientExpPartyMemberIndexes: number[] = [];
expPartyMembers.forEach((expPartyMember, epm) => {
if (expPartyMember.level <= medianLevel) {
recipientExpPartyMemberIndexes.push(epm);
}
});
const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length);
expPartyMembers.forEach((_partyMember, pm) => {
partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount());
});
}
for (let pm = 0; pm < expPartyMembers.length; pm++) {
const exp = partyMemberExp[pm];
if (exp) {
const partyMemberIndex = party.indexOf(expPartyMembers[pm]);
this.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this, partyMemberIndex, exp));
}
}
}
}
/**
* Loads or generates a mystery encounter
* @param override - used to load session encounter when restarting game, etc.
@ -2932,13 +3025,13 @@ export default class BattleScene extends SceneBase {
}
// Check for queued encounters first
if (!encounter && this.mysteryEncounterData?.nextEncounterQueue && this.mysteryEncounterData.nextEncounterQueue.length > 0) {
if (!encounter && this.mysteryEncounterSaveData?.queuedEncounters && this.mysteryEncounterSaveData.queuedEncounters.length > 0) {
let i = 0;
while (i < this.mysteryEncounterData.nextEncounterQueue.length && !!encounter) {
const candidate = this.mysteryEncounterData.nextEncounterQueue[i];
const forcedChance = candidate[1];
while (i < this.mysteryEncounterSaveData.queuedEncounters.length && !!encounter) {
const candidate = this.mysteryEncounterSaveData.queuedEncounters[i];
const forcedChance = candidate.spawnPercent;
if (Utils.randSeedInt(100) < forcedChance) {
encounter = allMysteryEncounters[candidate[0]];
encounter = allMysteryEncounters[candidate.type];
}
i++;
@ -2955,7 +3048,7 @@ export default class BattleScene extends SceneBase {
const tierWeights = [MysteryEncounterTier.COMMON, MysteryEncounterTier.GREAT, MysteryEncounterTier.ULTRA, MysteryEncounterTier.ROGUE];
// Adjust tier weights by previously encountered events to lower odds of only Common/Great in run
this.mysteryEncounterData.encounteredEvents.forEach(seenEncounterData => {
this.mysteryEncounterSaveData.encounteredEvents.forEach(seenEncounterData => {
if (seenEncounterData.tier === MysteryEncounterTier.COMMON) {
tierWeights[0] = tierWeights[0] - 6;
} else if (seenEncounterData.tier === MysteryEncounterTier.GREAT) {
@ -2976,7 +3069,7 @@ export default class BattleScene extends SceneBase {
let availableEncounters: MysteryEncounter[] = [];
// New encounter should never be the same as the most recent encounter
const previousEncounter = this.mysteryEncounterData.encounteredEvents.length > 0 ? this.mysteryEncounterData.encounteredEvents[this.mysteryEncounterData.encounteredEvents.length - 1].type : null;
const previousEncounter = this.mysteryEncounterSaveData.encounteredEvents.length > 0 ? this.mysteryEncounterSaveData.encounteredEvents[this.mysteryEncounterSaveData.encounteredEvents.length - 1].type : null;
const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? [];
// If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available
while (availableEncounters.length === 0 && tier !== null) {
@ -2995,9 +3088,9 @@ export default class BattleScene extends SceneBase {
if (previousEncounter !== null && encounterType === previousEncounter) { // Previous encounter was not this one
return false;
}
if (this.mysteryEncounterData.encounteredEvents.length > 0 && // Encounter has not exceeded max allowed encounters
if (this.mysteryEncounterSaveData.encounteredEvents.length > 0 && // Encounter has not exceeded max allowed encounters
(encounterCandidate.maxAllowedEncounters && encounterCandidate.maxAllowedEncounters > 0)
&& this.mysteryEncounterData.encounteredEvents.filter(e => e.type === encounterType).length >= encounterCandidate.maxAllowedEncounters) {
&& this.mysteryEncounterSaveData.encounteredEvents.filter(e => e.type === encounterType).length >= encounterCandidate.maxAllowedEncounters) {
return false;
}
return true;

View File

@ -129,7 +129,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
species: getPokemonSpecies(Species.BLACEPHALON),
mysteryEncounterData: new MysteryEncounterPokemonData(undefined, ability, undefined, [randSeedInt(18), randSeedInt(18)]),
mysteryEncounterPokemonData: new MysteryEncounterPokemonData(undefined, ability, undefined, [randSeedInt(18), randSeedInt(18)]),
isBoss: true,
moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN]
},
@ -344,10 +344,10 @@ export const ClowningAroundEncounter: MysteryEncounter =
}
}
newTypes.push(secondType);
if (!pokemon.mysteryEncounterData) {
pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, undefined, undefined, newTypes);
if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(undefined, undefined, undefined, newTypes);
} else {
pokemon.mysteryEncounterData.types = newTypes;
pokemon.mysteryEncounterPokemonData.types = newTypes;
}
}
})
@ -410,10 +410,10 @@ function displayYesNoOptions(scene: BattleScene, resolve) {
function onYesAbilitySwap(scene: BattleScene, resolve) {
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Do ability swap
if (!pokemon.mysteryEncounterData) {
pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, Abilities.AERILATE);
if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(undefined, Abilities.AERILATE);
}
pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter!.misc.ability;
pokemon.mysteryEncounterPokemonData.ability = scene.currentBattle.mysteryEncounter!.misc.ability;
scene.currentBattle.mysteryEncounter!.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));
};

View File

@ -76,7 +76,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
mysteryEncounterData: new MysteryEncounterPokemonData(1.25),
mysteryEncounterPokemonData: new MysteryEncounterPokemonData(1.25),
nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierConfigs: [

View File

@ -369,10 +369,10 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
newType = randSeedInt(18) as Type;
}
newTypes.push(newType);
if (!newPokemon.mysteryEncounterData) {
newPokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, undefined, undefined, newTypes);
if (!newPokemon.mysteryEncounterPokemonData) {
newPokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(undefined, undefined, undefined, newTypes);
} else {
newPokemon.mysteryEncounterData.types = newTypes;
newPokemon.mysteryEncounterPokemonData.types = newTypes;
}
for (const item of transformation.heldItems) {

View File

@ -123,11 +123,11 @@ export class PreviousEncounterRequirement extends EncounterSceneRequirement {
}
meetsRequirement(scene: BattleScene): boolean {
return scene.mysteryEncounterData.encounteredEvents.some(e => e.type === this.previousEncounterRequirement);
return scene.mysteryEncounterSaveData.encounteredEvents.some(e => e.type === this.previousEncounterRequirement);
}
getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
return ["previousEncounter", scene.mysteryEncounterData.encounteredEvents.find(e => e.type === this.previousEncounterRequirement)?.[0].toString() ?? ""];
return ["previousEncounter", scene.mysteryEncounterSaveData.encounteredEvents.find(e => e.type === this.previousEncounterRequirement)?.[0].toString() ?? ""];
}
}

View File

@ -7,20 +7,27 @@ export class SeenEncounterData {
type: MysteryEncounterType;
tier: MysteryEncounterTier;
waveIndex: number;
selectedOption: number;
constructor(type: MysteryEncounterType, tier: MysteryEncounterTier, waveIndex: number) {
constructor(type: MysteryEncounterType, tier: MysteryEncounterTier, waveIndex: number, selectedOption?: number) {
this.type = type;
this.tier = tier;
this.waveIndex = waveIndex;
this.selectedOption = selectedOption ?? -1;
}
}
export class MysteryEncounterData {
export interface QueuedEncounter {
type: MysteryEncounterType;
spawnPercent: number; // Out of 100
}
export class MysteryEncounterSaveData {
encounteredEvents: SeenEncounterData[] = [];
encounterSpawnChance: number = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
nextEncounterQueue: [MysteryEncounterType, integer][] = [];
queuedEncounters: QueuedEncounter[] = [];
constructor(data: MysteryEncounterData | null) {
constructor(data: MysteryEncounterSaveData | null) {
if (!isNullOrUndefined(data)) {
Object.assign(this, data);
}

View File

@ -72,7 +72,7 @@ export interface EnemyPokemonConfig {
isBoss: boolean;
bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number
mysteryEncounterData?: MysteryEncounterPokemonData;
mysteryEncounterPokemonData?: MysteryEncounterPokemonData;
formIndex?: number;
abilityIndex?: number;
level?: number;
@ -229,8 +229,8 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
}
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
if (!isNullOrUndefined(config.mysteryEncounterData)) {
enemyPokemon.mysteryEncounterData = config.mysteryEncounterData!;
if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) {
enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!;
}
// Set Boss

View File

@ -112,7 +112,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public battleData: PokemonBattleData;
public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData;
public mysteryEncounterData: MysteryEncounterPokemonData;
public mysteryEncounterPokemonData: MysteryEncounterPokemonData;
/** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */
public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -201,7 +201,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck;
this.usedTMs = dataSource.usedTMs ?? [];
this.mysteryEncounterData = dataSource.mysteryEncounterData ?? new MysteryEncounterPokemonData();
this.mysteryEncounterPokemonData = dataSource.mysteryEncounterPokemonData ?? new MysteryEncounterPokemonData();
} else {
this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id);
@ -249,7 +249,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.fusionLuck = this.luck;
this.mysteryEncounterData = new MysteryEncounterPokemonData();
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
}
this.generateName();
@ -577,8 +577,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const formKey = this.getFormKey();
if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) {
return 1.5;
} else if (!isNullOrUndefined(this.mysteryEncounterData.spriteScale) && this.mysteryEncounterData.spriteScale !== 0) {
return this.mysteryEncounterData.spriteScale!;
} else if (!isNullOrUndefined(this.mysteryEncounterPokemonData.spriteScale) && this.mysteryEncounterPokemonData.spriteScale !== 0) {
return this.mysteryEncounterPokemonData.spriteScale!;
}
return 1;
}
@ -1082,9 +1082,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
if (!types.length || !includeTeraType) {
if (this.mysteryEncounterData.types && this.mysteryEncounterData.types.length > 0) {
if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) {
// "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters
this.mysteryEncounterData.types.forEach(t => types.push(t));
this.mysteryEncounterPokemonData.types.forEach(t => types.push(t));
} else if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) {
this.summonData.types.forEach(t => types.push(t));
} else {
@ -1146,8 +1146,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
}
if (this.mysteryEncounterData?.ability) {
return allAbilities[this.mysteryEncounterData.ability];
if (this.mysteryEncounterPokemonData?.ability) {
return allAbilities[this.mysteryEncounterPokemonData.ability];
}
if (this.isFusion()) {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
@ -1173,8 +1173,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
}
if (this.mysteryEncounterData?.passive) {
return allAbilities[this.mysteryEncounterData.passive];
if (this.mysteryEncounterPokemonData?.passive) {
return allAbilities[this.mysteryEncounterPokemonData.passive];
}
let starterSpeciesId = this.species.speciesId;

View File

@ -8,7 +8,7 @@
"label": "Battle the Pokémon",
"tooltip": "(-) Tricky Battle\n(+) Strong Catchable Foe",
"selected": "You approach the\n{{enemyPokemon}} without fear.",
"stat_boost": "The {{enemyPokemon}} heightened abilities boost its stats!"
"stat_boost": "The {{enemyPokemon}}'s heightened abilities boost its stats!"
},
"2": {
"label": "Give It Food",

View File

@ -239,7 +239,7 @@ export class GameOverPhase extends BattlePhase {
timestamp: new Date().getTime(),
challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounter: this.scene.currentBattle.mysteryEncounter,
mysteryEncounterData: this.scene.mysteryEncounterData
mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData
} as SessionSaveData;
}
}

View File

@ -24,7 +24,7 @@ import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data";
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
/**
* Will handle (in order):
@ -63,7 +63,7 @@ export class MysteryEncounterPhase extends Phase {
if (!this.optionSelectSettings) {
// Sets flag that ME was encountered, only if this is not a followup option select phase
// Can be used in later MEs to check for requirements to spawn, run history, etc.
this.scene.mysteryEncounterData.encounteredEvents.push(new SeenEncounterData(encounter.encounterType, encounter.encounterTier, this.scene.currentBattle.waveIndex));
this.scene.mysteryEncounterSaveData.encounteredEvents.push(new SeenEncounterData(encounter.encounterType, encounter.encounterTier, this.scene.currentBattle.waveIndex));
}
// Initiates encounter dialogue window and option select
@ -74,6 +74,15 @@ export class MysteryEncounterPhase extends Phase {
// Set option selected flag
this.scene.currentBattle.mysteryEncounter!.selectedOption = option;
if (!this.optionSelectSettings) {
// Saves the selected option in the ME save data, only if this is not a followup option select phase
// Can be used for analytics purposes to track what options are popular on certain encounters
const encounterSaveData = this.scene.mysteryEncounterSaveData.encounteredEvents[this.scene.mysteryEncounterSaveData.encounteredEvents.length - 1];
if (encounterSaveData.type === this.scene.currentBattle.mysteryEncounter?.encounterType) {
encounterSaveData.selectedOption = index;
}
}
if (!option.onOptionPhase) {
return false;
}

View File

@ -0,0 +1,20 @@
import BattleScene from "#app/battle-scene.js";
import { Phase } from "#app/phase";
export class PartyExpPhase extends Phase {
expValue: number;
constructor(scene: BattleScene, expValue: number) {
super(scene);
this.expValue = expValue;
}
start() {
super.start();
this.scene.applyPartyExp(this.expValue);
this.end();
}
}

View File

@ -1,21 +1,15 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex, BattleType } from "#app/battle.js";
import { modifierTypes } from "#app/modifier/modifier-type.js";
import { ExpShareModifier, ExpBalanceModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier.js";
import * as Utils from "#app/utils.js";
import Overrides from "#app/overrides";
import { BattleEndPhase } from "./battle-end-phase";
import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase";
import { AddEnemyBuffModifierPhase } from "./add-enemy-buff-modifier-phase";
import { EggLapsePhase } from "./egg-lapse-phase";
import { ExpPhase } from "./exp-phase";
import { GameOverPhase } from "./game-over-phase";
import { ModifierRewardPhase } from "./modifier-reward-phase";
import { SelectModifierPhase } from "./select-modifier-phase";
import { ShowPartyExpBarPhase } from "./show-party-exp-bar-phase";
import { TrainerVictoryPhase } from "./trainer-victory-phase";
import { PokemonIncrementingStatModifier } from "#app/modifier/modifier";
import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
export class VictoryPhase extends PokemonPhase {
@ -33,94 +27,8 @@ export class VictoryPhase extends PokemonPhase {
this.scene.gameData.gameStats.pokemonDefeated++;
const participantIds = this.scene.currentBattle.playerParticipantIds;
const party = this.scene.getParty();
const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier;
const expBalanceModifier = this.scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
const multipleParticipantExpBonusModifier = this.scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier;
const nonFaintedPartyMembers = party.filter(p => p.hp);
const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.scene.getMaxExpLevel());
const partyMemberExp: number[] = [];
if (participantIds.size) {
let expValue = this.getPokemon().getExpValue();
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
expValue = Math.floor(expValue * 1.5);
} else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter) {
expValue = Math.floor(expValue * this.scene.currentBattle.mysteryEncounter.expMultiplier);
}
for (const partyMember of nonFaintedPartyMembers) {
const pId = partyMember.id;
const participated = participantIds.has(pId);
if (participated) {
partyMember.addFriendship(2);
const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier);
if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this.scene)) {
machoBraceModifier.stackCount++;
this.scene.updateModifiers(true, true);
partyMember.updateInfo();
}
}
if (!expPartyMembers.includes(partyMember)) {
continue;
}
if (!participated && !expShareModifier) {
partyMemberExp.push(0);
continue;
}
let expMultiplier = 0;
if (participated) {
expMultiplier += (1 / participantIds.size);
if (participantIds.size > 1 && multipleParticipantExpBonusModifier) {
expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2;
}
} else if (expShareModifier) {
expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size;
}
if (partyMember.pokerus) {
expMultiplier *= 1.5;
}
if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) {
expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE;
}
const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier);
this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp);
partyMemberExp.push(Math.floor(pokemonExp.value));
}
if (expBalanceModifier) {
let totalLevel = 0;
let totalExp = 0;
expPartyMembers.forEach((expPartyMember, epm) => {
totalExp += partyMemberExp[epm];
totalLevel += expPartyMember.level;
});
const medianLevel = Math.floor(totalLevel / expPartyMembers.length);
const recipientExpPartyMemberIndexes: number[] = [];
expPartyMembers.forEach((expPartyMember, epm) => {
if (expPartyMember.level <= medianLevel) {
recipientExpPartyMemberIndexes.push(epm);
}
});
const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length);
expPartyMembers.forEach((_partyMember, pm) => {
partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount());
});
}
for (let pm = 0; pm < expPartyMembers.length; pm++) {
const exp = partyMemberExp[pm];
if (exp) {
const partyMemberIndex = party.indexOf(expPartyMembers[pm]);
this.scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this.scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this.scene, partyMemberIndex, exp));
}
}
}
const expValue = this.getPokemon().getExpValue();
this.scene.applyPartyExp(expValue);
if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
handleMysteryEncounterVictory(this.scene, false, this.isExpOnly);

View File

@ -45,7 +45,7 @@ import { TerrainType } from "#app/data/terrain.js";
import { OutdatedPhase } from "#app/phases/outdated-phase.js";
import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js";
import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler";
import { MysteryEncounterData } from "../data/mystery-encounters/mystery-encounter-data";
import { MysteryEncounterSaveData } from "../data/mystery-encounters/mystery-encounter-save-data";
import MysteryEncounter from "../data/mystery-encounters/mystery-encounter";
export const defaultStarterSpecies: Species[] = [
@ -132,7 +132,7 @@ export interface SessionSaveData {
timestamp: integer;
challenges: ChallengeData[];
mysteryEncounter: MysteryEncounter;
mysteryEncounterData: MysteryEncounterData;
mysteryEncounterSaveData: MysteryEncounterSaveData;
}
interface Unlocks {
@ -978,7 +978,7 @@ export class GameData {
timestamp: new Date().getTime(),
challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounter: scene.currentBattle.mysteryEncounter,
mysteryEncounterData: scene.mysteryEncounterData
mysteryEncounterSaveData: scene.mysteryEncounterSaveData
} as SessionSaveData;
}
@ -1069,7 +1069,7 @@ export class GameData {
scene.score = sessionData.score;
scene.updateScoreText();
scene.mysteryEncounterData = sessionData?.mysteryEncounterData ? sessionData?.mysteryEncounterData : new MysteryEncounterData(null);
scene.mysteryEncounterSaveData = sessionData?.mysteryEncounterSaveData ?? new MysteryEncounterSaveData(null);
scene.newArena(sessionData.arena.biome);
@ -1294,8 +1294,8 @@ export class GameData {
return new MysteryEncounter(v);
}
if (k === "mysteryEncounterData") {
return new MysteryEncounterData(v);
if (k === "mysteryEncounterSaveData") {
return new MysteryEncounterSaveData(v);
}
return v;

View File

@ -57,7 +57,7 @@ export default class PokemonData {
public bossSegments?: integer;
public summonData: PokemonSummonData;
public mysteryEncounterData: MysteryEncounterPokemonData;
public mysteryEncounterPokemonData: MysteryEncounterPokemonData;
constructor(source: Pokemon | any, forHistory: boolean = false) {
const sourcePokemon = source instanceof Pokemon ? source : null;
@ -103,7 +103,7 @@ export default class PokemonData {
this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0);
this.usedTMs = source.usedTMs ?? [];
this.mysteryEncounterData = source.mysteryEncounterData ?? new MysteryEncounterPokemonData();
this.mysteryEncounterPokemonData = source.mysteryEncounterPokemonData ?? new MysteryEncounterPokemonData();
if (!forHistory) {
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);

View File

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

View File

@ -121,7 +121,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
mysteryEncounterData: new MysteryEncounterPokemonData(1.25),
mysteryEncounterPokemonData: new MysteryEncounterPokemonData(1.25),
nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierConfigs: expect.any(Array),

View File

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

View File

@ -49,9 +49,9 @@ describe("Mystery Encounter Phases", () => {
});
await game.phaseInterceptor.run(MysteryEncounterPhase);
expect(game.scene.mysteryEncounterData.encounteredEvents.length).toBeGreaterThan(0);
expect(game.scene.mysteryEncounterData.encounteredEvents[0].type).toEqual(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
expect(game.scene.mysteryEncounterData.encounteredEvents[0].tier).toEqual(MysteryEncounterTier.GREAT);
expect(game.scene.mysteryEncounterSaveData.encounteredEvents.length).toBeGreaterThan(0);
expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].type).toEqual(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].tier).toEqual(MysteryEncounterTier.GREAT);
expect(game.scene.ui.getMode()).toBe(Mode.MYSTERY_ENCOUNTER);
});