From 2318d86cb1dcf1f2dde338313d5ac25546d39b4c Mon Sep 17 00:00:00 2001 From: ImperialSympathizer <110984302+ben-lear@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:43:53 -0400 Subject: [PATCH] Mystery Encounters (#2824) * squash commits to clean up PR * add chest sprites * bug fixes, cleanup, and tests * small fixes --------- Co-authored-by: ImperialSympathizer --- src/battle-scene.ts | 10 +- src/modifier/modifier-type.ts | 12 +- src/phases.ts | 279 ++++++++-------------------------- src/test/vitest.setup.ts | 6 +- 4 files changed, 75 insertions(+), 232 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 3fe0d6765db..0515800e53f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -93,13 +93,8 @@ import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager.js"; import i18next from "i18next"; import MysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounter"; -import { - mysteryEncountersByBiome, - allMysteryEncounters, - BASE_MYSTERY_ENCOUNTER_WEIGHT, - AVERAGE_ENCOUNTERS_PER_RUN_TARGET -} from "./data/mystery-encounters/mystery-encounters"; -import {MysteryEncounterFlags} from "#app/data/mystery-encounter-flags"; +import { mysteryEncountersByBiome, allMysteryEncounters, BASE_MYSTERY_ENCOUNTER_WEIGHT, AVERAGE_ENCOUNTERS_PER_RUN_TARGET } from "./data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterFlags } from "#app/data/mystery-encounter-flags"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -1225,6 +1220,7 @@ export default class BattleScene extends SceneBase { pokemon.resetBattleData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } + } this.unshiftPhase(new ShowTrainerPhase(this)); } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 308b08b34cb..250b2953fdf 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1818,7 +1818,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo const retryCount = Math.min(count * 5, 50); if (!customModifierSettings) { new Array(count).fill(0).map((_, i) => { - options.push(getModifierTypeOptionWithLuckUpgrades(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined)); + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined)); }); } else { // Guaranteed mods first @@ -1857,14 +1857,14 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo // Guaranteed tiers third if (customModifierSettings?.guaranteedModifierTiers?.length) { customModifierSettings?.guaranteedModifierTiers.forEach((tier) => { - options.push(getModifierTypeOptionWithLuckUpgrades(options, retryCount, party, tier)); + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier)); }); } // Fill remaining if (options.length < count && customModifierSettings.fillRemaining) { while (options.length < count) { - options.push(getModifierTypeOptionWithLuckUpgrades(options, retryCount, party, undefined)); + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, undefined)); } } } @@ -1880,10 +1880,10 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo return options; } -function getModifierTypeOptionWithLuckUpgrades(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier): ModifierTypeOption { - let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, modifierTiers?.length > i ? modifierTiers[i] : undefined); +function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier): ModifierTypeOption { + let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier); let r = 0; - while (options.length && ++r < retryCount && options.filter(o => o.type.name === candidate.type.name || o.type.group === candidate.type.group).length) { + while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate.type.name || o.type.group === candidate.type.group).length) { candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate.type.tier, candidate.upgradeCount); } return candidate; diff --git a/src/phases.ts b/src/phases.ts index db060d8fe2d..c6cda964e18 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -5,7 +5,7 @@ import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMov import { Mode } from "./ui/ui"; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; -import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, PokemonMoveAccuracyBoosterModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier } from "./modifier/modifier"; +import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, PokemonMoveAccuracyBoosterModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier } from "./modifier/modifier"; import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball"; import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims"; @@ -19,7 +19,7 @@ import { biomeLinks, getBiomeName } from "./data/biomes"; import { ModifierTier } from "./modifier/modifier-tier"; import { ModifierPoolType, ModifierType, ModifierTypeFunc, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags"; +import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag, MysteryEncounterPostSummonTag } from "./data/battler-tags"; import { getPokemonMessage, getPokemonNameWithAffix } from "./messages"; import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; @@ -40,7 +40,6 @@ import { SessionSaveData } from "./system/game-data"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims"; import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue"; -import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler"; import { SettingKeys } from "./system/settings/settings"; import { Tutorial, handleTutorial } from "./tutorial"; import { TerrainType } from "./data/terrain"; @@ -296,6 +295,14 @@ export class TitlePhase extends Phase { }, keepOpen: true }, + { + label: i18next.t("menu:settings"), + handler: () => { + this.scene.ui.setOverlayMode(Mode.SETTINGS); + return true; + }, + keepOpen: true + }, { label: i18next.t("menu:settings"), handler: () => { @@ -1471,7 +1478,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { preSummon(): void { const partyMember = this.getPokemon(); // If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon - if (!partyMember.isAllowedInBattle() || (this.player && isNullOrUndefined(this.getParty().find(p => p.id === partyMember.id)))) { + if (!partyMember.isAllowedInBattle() || (this.player && !this.getParty().some(p => p.id === partyMember.id))) { console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve..."); // First check if they're somehow still in play, if so remove them. @@ -1661,6 +1668,57 @@ export class SummonPhase extends PartyMemberPokemonPhase { }); } + summonWild(): void { + const pokemon = this.getPokemon(); + + if (this.fieldIndex === 1) { + pokemon.setFieldPosition(FieldPosition.RIGHT, 0); + } else { + const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length; + pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); + } + + this.scene.add.existing(pokemon); + this.scene.field.add(pokemon); + if (!this.player) { + const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; + if (playerPokemon?.visible) { + this.scene.field.moveBelow(pokemon, playerPokemon); + } + this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); + } + this.scene.updateModifiers(this.player); + this.scene.updateFieldScale(); + pokemon.showInfo(); + pokemon.playAnim(); + pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); + pokemon.setScale(0.75); + pokemon.tint(getPokeballTintColor(pokemon.pokeball)); + pokemon.untint(250, "Sine.easeIn"); + this.scene.updateFieldScale(); + pokemon.x += 16; + pokemon.y -= 16; + pokemon.alpha = 0; + + // Ease pokemon in + this.scene.tweens.add({ + targets: pokemon, + x: "-=16", + y: "+=16", + alpha: 1, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + pokemon.getSprite().clearTint(); + pokemon.resetSummonData(); + this.scene.time.delayedCall(1000, () => this.end()); + } + }); + } + onEnd(): void { const pokemon = this.getPokemon(); @@ -5246,219 +5304,6 @@ export class AttemptRunPhase extends PokemonPhase { } } -export class SelectModifierPhase extends BattlePhase { - private rerollCount: integer; - private modifierTiers: ModifierTier[]; - - constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[]) { - super(scene); - - this.rerollCount = rerollCount; - this.modifierTiers = modifierTiers; - } - - start() { - super.start(); - - if (!this.rerollCount) { - this.updateSeed(); - } else { - this.scene.reroll = false; - } - - const party = this.scene.getParty(); - regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); - const modifierCount = new Utils.IntegerHolder(3); - if (this.isPlayer()) { - this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); - } - const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); - - const modifierSelectCallback = (rowCursor: integer, cursor: integer) => { - if (rowCursor < 0 || cursor < 0) { - this.scene.ui.showText(i18next.t("battle:skipItemQuestion"), null, () => { - this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { - this.scene.ui.revertMode(); - this.scene.ui.setMode(Mode.MESSAGE); - super.end(); - }, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers))); - }); - return false; - } - let modifierType: ModifierType; - let cost: integer; - switch (rowCursor) { - case 0: - switch (cursor) { - case 0: - const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); - if (this.scene.money < rerollCost) { - this.scene.ui.playError(); - return false; - } else { - this.scene.reroll = true; - this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type.tier))); - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); - this.scene.money -= rerollCost; - this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); - this.scene.playSound("buy"); - } - break; - case 1: - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { - if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { - const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; - const itemModifier = itemModifiers[itemIndex]; - this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); - } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - }, PartyUiHandler.FilterItemMaxStacks); - break; - case 2: - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - }); - break; - case 3: - this.scene.lockModifierTiers = !this.scene.lockModifierTiers; - const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; - uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - uiHandler.updateLockRaritiesText(); - uiHandler.updateRerollCostText(); - return false; - } - return true; - case 1: - modifierType = typeOptions[cursor].type; - break; - default: - const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)); - const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT]; - modifierType = shopOption.type; - cost = shopOption.cost; - break; - } - - if (cost && this.scene.money < cost) { - this.scene.ui.playError(); - return false; - } - - const applyModifier = (modifier: Modifier, playSound: boolean = false) => { - const result = this.scene.addModifier(modifier, false, playSound); - if (cost) { - result.then(success => { - if (success) { - this.scene.money -= cost; - this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); - this.scene.playSound("buy"); - (this.scene.ui.getHandler() as ModifierSelectUiHandler).updateCostText(); - } else { - this.scene.ui.playError(); - } - }); - } else { - const doEnd = () => { - this.scene.ui.clearText(); - this.scene.ui.setMode(Mode.MESSAGE); - super.end(); - }; - if (result instanceof Promise) { - result.then(() => doEnd()); - } else { - doEnd(); - } - } - }; - - if (modifierType instanceof PokemonModifierType) { - if (modifierType instanceof FusePokemonModifierType) { - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.SPLICE, -1, (fromSlotIndex: integer, spliceSlotIndex: integer) => { - if (spliceSlotIndex !== undefined && fromSlotIndex < 6 && spliceSlotIndex < 6 && fromSlotIndex !== spliceSlotIndex) { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => { - const modifier = modifierType.newModifier(party[fromSlotIndex], party[spliceSlotIndex]); - applyModifier(modifier, true); - }); - } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - }, modifierType.selectFilter); - } else { - const pokemonModifierType = modifierType as PokemonModifierType; - const isMoveModifier = modifierType instanceof PokemonMoveModifierType; - const isTmModifier = modifierType instanceof TmModifierType; - const isRememberMoveModifier = modifierType instanceof RememberMoveModifierType; - const isPpRestoreModifier = (modifierType instanceof PokemonPpRestoreModifierType || modifierType instanceof PokemonPpUpModifierType); - const partyUiMode = isMoveModifier ? PartyUiMode.MOVE_MODIFIER - : isTmModifier ? PartyUiMode.TM_MODIFIER - : isRememberMoveModifier ? PartyUiMode.REMEMBER_MOVE_MODIFIER - : PartyUiMode.MODIFIER; - const tmMoveId = isTmModifier - ? (modifierType as TmModifierType).moveId - : undefined; - this.scene.ui.setModeWithoutClear(Mode.PARTY, partyUiMode, -1, (slotIndex: integer, option: PartyOption) => { - if (slotIndex < 6) { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => { - const modifier = !isMoveModifier - ? !isRememberMoveModifier - ? modifierType.newModifier(party[slotIndex]) - : modifierType.newModifier(party[slotIndex], option as integer) - : modifierType.newModifier(party[slotIndex], option - PartyOption.MOVE_1); - applyModifier(modifier, true); - }); - } else { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - }, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier); - } - } else { - applyModifier(modifierType.newModifier()); - } - - return !cost; - }; - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - } - - updateSeed(): void { - this.scene.resetSeed(); - } - - isPlayer(): boolean { - return true; - } - - getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): integer { - let baseValue = 0; - if (lockRarities) { - const tierValues = [50, 125, 300, 750, 2000]; - for (const opt of typeOptions) { - baseValue += tierValues[opt.type.tier]; - } - } else { - baseValue = 250; - } - return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount), Number.MAX_SAFE_INTEGER); - } - - getPoolType(): ModifierPoolType { - return ModifierPoolType.PLAYER; - } - - getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] { - return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined); - } - - addModifier(modifier: Modifier): Promise { - return this.scene.addModifier(modifier, false, true); - } -} - export class EggLapsePhase extends Phase { constructor(scene: BattleScene) { super(scene); diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index 2b2be3bb42b..3615ca7d27e 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -14,9 +14,11 @@ import { initVouchers } from "#app/system/voucher.js"; import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; import { beforeAll, beforeEach, vi } from "vitest"; import * as overrides from "#app/overrides"; -import {initMysteryEncounterDialogue} from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue"; -import {initMysteryEncounters} from "#app/data/mystery-encounters/mystery-encounters"; +import { initMysteryEncounterDialogue } from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue"; +import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; +initVouchers(); +initAchievements(); initStatsKeys(); initPokemonPrevolutions(); initBiomes();