From 0db39f9a1d3b050b1498503d487b08540e338a46 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sat, 7 Sep 2024 20:56:47 -0400 Subject: [PATCH] ME data schema updates --- .github/pull_request_template.md | 2 +- src/battle-scene.ts | 125 +++++++++++++++--- .../encounters/clowning-around-encounter.ts | 14 +- .../encounters/the-strong-stuff-encounter.ts | 2 +- .../encounters/weird-dream-encounter.ts | 6 +- .../mystery-encounter-requirements.ts | 4 +- ...data.ts => mystery-encounter-save-data.ts} | 15 ++- .../utils/encounter-phase-utils.ts | 6 +- src/field/pokemon.ts | 22 +-- .../uncommon-breed-dialogue.json | 2 +- src/phases/game-over-phase.ts | 2 +- src/phases/mystery-encounter-phases.ts | 13 +- src/phases/party-exp-phase.ts | 20 +++ src/phases/victory-phase.ts | 96 +------------- src/system/game-data.ts | 12 +- src/system/pokemon-data.ts | 4 +- .../clowning-around-encounter.test.ts | 16 +-- .../the-strong-stuff-encounter.test.ts | 2 +- .../encounters/weird-dream-encounter.test.ts | 2 +- .../phases/mystery-encounter-phase.test.ts | 6 +- 20 files changed, 204 insertions(+), 167 deletions(-) rename src/data/mystery-encounters/{mystery-encounter-data.ts => mystery-encounter-save-data.ts} (66%) create mode 100644 src/phases/party-exp-phase.ts diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3e6b8bf6d0d..a30cb642a46 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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? diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 4ca785faf99..6ac6793138d 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -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; diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 2454526da4a..0b6e6a4314c 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -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)); }; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 91bb8d47752..006ca4535cb 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -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: [ diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 42850d4ef84..3d8b078e6b7 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -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) { diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index fbeaccd50f9..53eaa162dc2 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -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() ?? ""]; } } diff --git a/src/data/mystery-encounters/mystery-encounter-data.ts b/src/data/mystery-encounters/mystery-encounter-save-data.ts similarity index 66% rename from src/data/mystery-encounters/mystery-encounter-data.ts rename to src/data/mystery-encounters/mystery-encounter-save-data.ts index a16dc5ecb7d..c91f0857f83 100644 --- a/src/data/mystery-encounters/mystery-encounter-data.ts +++ b/src/data/mystery-encounters/mystery-encounter-save-data.ts @@ -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); } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index d5c246934d1..d1877482857 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -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 diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2362c3bdda6..d885b90379e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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; diff --git a/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json index a1fef9a0001..e6f5b3d3fcd 100644 --- a/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json +++ b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json @@ -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", diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index b6f7cd579e9..2c8777ecfe1 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -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; } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index e8a1079eb8d..ad3a5d150a8 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -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; } diff --git a/src/phases/party-exp-phase.ts b/src/phases/party-exp-phase.ts new file mode 100644 index 00000000000..b5d85b187c1 --- /dev/null +++ b/src/phases/party-exp-phase.ts @@ -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(); + } +} diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 4c0ce43eb76..85bdca71171 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -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); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8688fed7a6c..774cbbcaeca 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -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; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 650b7d1950d..68b9b768be4 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -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); diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 85b50f9758c..383e3bd3564 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -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); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index edaf80f86c7..0600005aa52 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -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), diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index 635dac0eaee..ef014c6949b 100644 --- a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -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); diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts index 32a675b526a..0a99cd00db3 100644 --- a/src/test/phases/mystery-encounter-phase.test.ts +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -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); });