From 7e3dca57b4d208f021e70a20188fb4e4d4e02cba Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 08:22:16 -0700 Subject: [PATCH] Manual merging --- src/phases.ts | 1113 +++++++++++++++++++++++++-------------- src/system/game-data.ts | 540 +++++++++++++------ 2 files changed, 1102 insertions(+), 551 deletions(-) diff --git a/src/phases.ts b/src/phases.ts index a259917a4bc..17a116e5c42 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,12 +1,11 @@ import BattleScene, { bypassLogin } from "./battle-scene"; import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from "./utils"; -import { Moves } from "./data/enums/moves"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; 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 } from "./modifier/modifier"; +import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, PokemonResetNegativeStatStageModifier } 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"; @@ -17,53 +16,57 @@ import { EvolutionPhase } from "./evolution-phase"; import { Phase } from "./phase"; import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "./data/battle-stat"; import { biomeLinks, getBiomeName } from "./data/biomes"; -import { Biome } from "./data/enums/biome"; import { ModifierTier } from "./modifier/modifier-tier"; import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { BattlerTagLapseType, EncoreTag, HideSpriteTag as HiddenTag, ProtectedTag, TrappedTag } from "./data/battler-tags"; -import { BattlerTagType } from "./data/enums/battler-tag-type"; -import { getPokemonMessage, getPokemonPrefix } from "./messages"; +import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags"; +import { getPokemonMessage, getPokemonNameWithAffix } from "./messages"; import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; -import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { ArenaTagType } from "./data/enums/arena-tag-type"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, HealFromBerryUseAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; -import { BattleSpec } from "./enums/battle-spec"; -import { Species } from "./data/enums/species"; -import { HealAchv, LevelAchv, achvs } from "./system/achv"; +import { ChallengeAchv, HealAchv, LevelAchv, achvs } from "./system/achv"; import { TrainerSlot, trainerConfigs } from "./data/trainer-config"; -import { TrainerType } from "./data/enums/trainer-type"; import { EggHatchPhase } from "./egg-hatch-phase"; import { Egg } from "./data/egg"; import { vouchers } from "./system/voucher"; -import { loggedInUser, updateUserInfo } from "./account"; -import { PlayerGender, SessionSaveData, saveRunHistory } from "./system/game-data"; +import { clientSessionId, loggedInUser, updateUserInfo } from "./account"; +import { SessionSaveData } from "./system/game-data"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims"; -import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; +import { SpeciesFormChangeActiveTrigger, 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 { Setting } from "./system/settings"; +import { SettingKeys } from "./system/settings/settings"; import { Tutorial, handleTutorial } from "./tutorial"; import { TerrainType } from "./data/terrain"; import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; import { SaveSlotUiMode } from "./ui/save-slot-select-ui-handler"; import { fetchDailyRunSeed, getDailyRunStarters } from "./data/daily-run"; -import { GameModes, gameModes } from "./game-mode"; +import { GameMode, GameModes, getGameMode } from "./game-mode"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species"; import i18next from "./plugins/i18n"; -import { Abilities } from "./data/enums/abilities"; -import * as Overrides from "./overrides"; +import Overrides from "#app/overrides"; import { TextStyle, addTextObject } from "./ui/text"; import { Type } from "./data/type"; -import { MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./battle-scene-events"; -import { runCount } from "./ui/run-history-ui-handler"; +import { BerryUsedEvent, EncounterPhaseEvent, MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./events/battle-scene"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattleSpec } from "#enums/battle-spec"; +import { BattleStyle } from "#enums/battle-style"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Biome } from "#enums/biome"; +import { ExpNotification } from "#enums/exp-notification"; +import { Moves } from "#enums/moves"; +import { PlayerGender } from "#enums/player-gender"; +import { Species } from "#enums/species"; +import { TrainerType } from "#enums/trainer-type"; +import { applyChallenges, ChallengeType } from "./data/challenge"; +const { t } = i18next; export class LoginPhase extends Phase { private showText: boolean; @@ -92,7 +95,14 @@ export class LoginPhase extends Phase { this.scene.playSound("menu_open"); const loadData = () => { - updateUserInfo().then(() => this.scene.gameData.loadSystem().then(() => this.end())); + updateUserInfo().then(success => { + if (!success[0]) { + Utils.removeCookie(Utils.sessionIdKey); + this.scene.reset(true, true); + return; + } + this.scene.gameData.loadSystem().then(() => this.end()); + }); }; this.scene.ui.setMode(Mode.LOGIN_FORM, { @@ -106,16 +116,36 @@ export class LoginPhase extends Phase { buttonActions: [ () => { this.scene.ui.playSelect(); - updateUserInfo().then(() => this.end()); + updateUserInfo().then(success => { + if (!success[0]) { + Utils.removeCookie(Utils.sessionIdKey); + this.scene.reset(true, true); + return; + } + this.end(); + } ); }, () => { this.scene.unshiftPhase(new LoginPhase(this.scene, false)); this.end(); } ] }); + }, () => { + const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); + const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID; + const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`; + window.open(discordUrl, "_self"); + }, () => { + const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`); + const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID; + const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; + window.open(googleUrl, "_self"); } ] }); + } else if (statusCode === 401) { + Utils.removeCookie(Utils.sessionIdKey); + this.scene.reset(true, true); } else { this.scene.unshiftPhase(new UnavailablePhase(this.scene)); super.end(); @@ -127,7 +157,7 @@ export class LoginPhase extends Phase { this.end(); } else { this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(i18next.t("menu:failedToLoadSaveData")); + this.scene.ui.showText(t("menu:failedToLoadSaveData")); } }); } @@ -148,7 +178,7 @@ export class LoginPhase extends Phase { export class TitlePhase extends Phase { private loaded: boolean; private lastSessionData: SessionSaveData; - private gameMode: GameModes; + public gameMode: GameModes; constructor(scene: BattleScene) { super(scene); @@ -182,7 +212,7 @@ export class TitlePhase extends Phase { const options: OptionSelectItem[] = []; if (loggedInUser.lastSessionSlot > -1) { options.push({ - label: i18next.t("menu:continue"), + label: i18next.t("continue", null, { ns: "menu"}), handler: () => { this.loadSaveSlot(this.lastSessionData ? -1 : loggedInUser.lastSessionSlot); return true; @@ -201,14 +231,21 @@ export class TitlePhase extends Phase { if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { const options: OptionSelectItem[] = [ { - label: gameModes[GameModes.CLASSIC].getName(), + label: GameMode.getModeName(GameModes.CLASSIC), handler: () => { setModeAndEnd(GameModes.CLASSIC); return true; } }, { - label: gameModes[GameModes.ENDLESS].getName(), + label: GameMode.getModeName(GameModes.CHALLENGE), + handler: () => { + setModeAndEnd(GameModes.CHALLENGE); + return true; + } + }, + { + label: GameMode.getModeName(GameModes.ENDLESS), handler: () => { setModeAndEnd(GameModes.ENDLESS); return true; @@ -217,7 +254,7 @@ export class TitlePhase extends Phase { ]; if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { options.push({ - label: gameModes[GameModes.SPLICED_ENDLESS].getName(), + label: GameMode.getModeName(GameModes.SPLICED_ENDLESS), handler: () => { setModeAndEnd(GameModes.SPLICED_ENDLESS); return true; @@ -263,6 +300,14 @@ export class TitlePhase extends Phase { return true; }, keepOpen: true + }, + { + label: i18next.t("menu:settings"), + handler: () => { + this.scene.ui.setOverlayMode(Mode.SETTINGS); + return true; + }, + keepOpen: true }); const config: OptionSelectConfig = { options: options, @@ -298,7 +343,7 @@ export class TitlePhase extends Phase { this.scene.sessionSlotId = slotId; const generateDaily = (seed: string) => { - this.scene.gameMode = gameModes[GameModes.DAILY]; + this.scene.gameMode = getGameMode(GameModes.DAILY); this.scene.setSeed(seed); this.scene.resetSeed(1); @@ -360,7 +405,12 @@ export class TitlePhase extends Phase { end(): void { if (!this.loaded && !this.scene.gameMode.isDaily) { this.scene.arena.preloadBgm(); - this.scene.pushPhase(new SelectStarterPhase(this.scene, this.gameMode)); + this.scene.gameMode = getGameMode(this.gameMode); + if (this.gameMode === GameModes.CHALLENGE) { + this.scene.pushPhase(new SelectChallengePhase(this.scene)); + } else { + this.scene.pushPhase(new SelectStarterPhase(this.scene)); + } this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene)); } else { this.scene.playBgm(); @@ -369,7 +419,7 @@ export class TitlePhase extends Phase { this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded)); if (this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()).length; + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -467,19 +517,19 @@ export class SelectGenderPhase extends Phase { this.scene.ui.setMode(Mode.OPTION_SELECT, { options: [ { - label: i18next.t("menu:boy"), + label: i18next.t("settings:boy"), handler: () => { this.scene.gameData.gender = PlayerGender.MALE; - this.scene.gameData.saveSetting(Setting.Player_Gender, 0); + this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 0); this.scene.gameData.saveSystem().then(() => this.end()); return true; } }, { - label: i18next.t("menu:girl"), + label: i18next.t("settings:girl"), handler: () => { this.scene.gameData.gender = PlayerGender.FEMALE; - this.scene.gameData.saveSetting(Setting.Player_Gender, 1); + this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 1); this.scene.gameData.saveSystem().then(() => this.end()); return true; } @@ -495,13 +545,24 @@ export class SelectGenderPhase extends Phase { } } -export class SelectStarterPhase extends Phase { - private gameMode: GameModes; - - constructor(scene: BattleScene, gameMode: GameModes) { +export class SelectChallengePhase extends Phase { + constructor(scene: BattleScene) { super(scene); + } - this.gameMode = gameMode; + start() { + super.start(); + + this.scene.playBgm("menu"); + + this.scene.ui.setMode(Mode.CHALLENGE_SELECT); + } +} + +export class SelectStarterPhase extends Phase { + + constructor(scene: BattleScene) { + super(scene); } start() { @@ -518,59 +579,71 @@ export class SelectStarterPhase extends Phase { return this.end(); } this.scene.sessionSlotId = slotId; - - const party = this.scene.getParty(); - const loadPokemonAssets: Promise[] = []; - starters.forEach((starter: Starter, i: integer) => { - if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as Species); - } - const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); - if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starterFormIndex = Overrides.STARTER_FORM_OVERRIDE; - } - let starterGender = starter.species.malePercent !== null - ? !starterProps.female ? Gender.MALE : Gender.FEMALE - : Gender.GENDERLESS; - if (Overrides.GENDER_OVERRIDE !== null) { - starterGender = Overrides.GENDER_OVERRIDE; - } - const starterIvs = this.scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); - const starterPokemon = this.scene.addPlayerPokemon(starter.species, this.scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); - starterPokemon.tryPopulateMoveset(starter.moveset); - if (starter.passive) { - starterPokemon.passive = true; - } - starterPokemon.luck = this.scene.gameData.getDexAttrLuck(this.scene.gameData.dexData[starter.species.speciesId].caughtAttr); - if (starter.pokerus) { - starterPokemon.pokerus = true; - } - if (this.scene.gameMode.isSplicedOnly) { - starterPokemon.generateFusionSpecies(true); - } - starterPokemon.setVisible(false); - party.push(starterPokemon); - loadPokemonAssets.push(starterPokemon.loadAssets()); - }); - overrideModifiers(this.scene); - overrideHeldItems(this.scene, party[0]); - Promise.all(loadPokemonAssets).then(() => { - SoundFade.fadeOut(this.scene, this.scene.sound.get("menu"), 500, true); - this.scene.time.delayedCall(500, () => this.scene.playBgm()); - if (this.scene.gameMode.isClassic) { - this.scene.gameData.gameStats.classicSessionsPlayed++; - } else { - this.scene.gameData.gameStats.endlessSessionsPlayed++; - } - this.scene.newBattle(); - this.scene.arena.init(); - this.scene.sessionPlayTime = 0; - this.scene.lastSavePlayTime = 0; - this.end(); - }); + this.initBattle(starters); }); - }, this.gameMode); + }); + } + + /** + * Initialize starters before starting the first battle + * @param starters {@linkcode Pokemon} with which to start the first battle + */ + initBattle(starters: Starter[]) { + const party = this.scene.getParty(); + const loadPokemonAssets: Promise[] = []; + starters.forEach((starter: Starter, i: integer) => { + if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { + starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as Species); + } + const starterProps = this.scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); + let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + if ( + starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES && + starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]] + ) { + starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]; + } + + let starterGender = starter.species.malePercent !== null + ? !starterProps.female ? Gender.MALE : Gender.FEMALE + : Gender.GENDERLESS; + if (Overrides.GENDER_OVERRIDE !== null) { + starterGender = Overrides.GENDER_OVERRIDE; + } + const starterIvs = this.scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); + const starterPokemon = this.scene.addPlayerPokemon(starter.species, this.scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); + starterPokemon.tryPopulateMoveset(starter.moveset); + if (starter.passive) { + starterPokemon.passive = true; + } + starterPokemon.luck = this.scene.gameData.getDexAttrLuck(this.scene.gameData.dexData[starter.species.speciesId].caughtAttr); + if (starter.pokerus) { + starterPokemon.pokerus = true; + } + if (this.scene.gameMode.isSplicedOnly) { + starterPokemon.generateFusionSpecies(true); + } + starterPokemon.setVisible(false); + applyChallenges(this.scene.gameMode, ChallengeType.STARTER_MODIFY, starterPokemon); + party.push(starterPokemon); + loadPokemonAssets.push(starterPokemon.loadAssets()); + }); + overrideModifiers(this.scene); + overrideHeldItems(this.scene, party[0]); + Promise.all(loadPokemonAssets).then(() => { + SoundFade.fadeOut(this.scene, this.scene.sound.get("menu"), 500, true); + this.scene.time.delayedCall(500, () => this.scene.playBgm()); + if (this.scene.gameMode.isClassic) { + this.scene.gameData.gameStats.classicSessionsPlayed++; + } else { + this.scene.gameData.gameStats.endlessSessionsPlayed++; + } + this.scene.newBattle(); + this.scene.arena.init(); + this.scene.sessionPlayTime = 0; + this.scene.lastSavePlayTime = 0; + this.end(); + }); } } @@ -584,7 +657,7 @@ export class BattlePhase extends Phase { const tintSprites = this.scene.currentBattle.trainer.getTintSprites(); for (let i = 0; i < sprites.length; i++) { const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2; - [ sprites[i], tintSprites[i] ].map(sprite => { + [sprites[i], tintSprites[i]].map(sprite => { if (visible) { sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16; } @@ -625,11 +698,19 @@ export abstract class FieldPhase extends BattlePhase { const playerField = this.scene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; - let orderedTargets: Pokemon[] = playerField.concat(enemyField).sort((a: Pokemon, b: Pokemon) => { + // We shuffle the list before sorting so speed ties produce random results + let orderedTargets: Pokemon[] = playerField.concat(enemyField); + // We seed it with the current turn to prevent an inconsistency where it + // was varying based on how long since you last reloaded + this.scene.executeWithSeedOffset(() => { + orderedTargets = Utils.randSeedShuffle(orderedTargets); + }, this.scene.currentBattle.turn, this.scene.waveSeed); + + orderedTargets.sort((a: Pokemon, b: Pokemon) => { const aSpeed = a?.getBattleStat(Stat.SPD) || 0; const bSpeed = b?.getBattleStat(Stat.SPD) || 0; - return aSpeed < bSpeed ? 1 : aSpeed > bSpeed ? -1 : !this.scene.randBattleSeedInt(2) ? -1 : 1; + return bSpeed - aSpeed; }); const speedReversed = new Utils.BooleanHolder(false); @@ -733,6 +814,8 @@ export class EncounterPhase extends BattlePhase { this.scene.initSession(); + this.scene.eventTarget.dispatchEvent(new EncounterPhaseEvent()); + // Failsafe if players somehow skip floor 200 in classic mode if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) { this.scene.unshiftPhase(new GameOverPhase(this.scene)); @@ -786,7 +869,7 @@ export class EncounterPhase extends BattlePhase { loadEnemyAssets.push(enemyPokemon.loadAssets()); - console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats); + console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats); }); if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { @@ -796,9 +879,11 @@ export class EncounterPhase extends BattlePhase { if (battle.battleType === BattleType.TRAINER) { loadEnemyAssets.push(battle.trainer.loadAssets().then(() => battle.trainer.initSprite())); } else { + // This block only applies for double battles to init the boss segments (idk why it's split up like this) if (battle.enemyParty.filter(p => p.isBoss()).length > 1) { for (const enemyPokemon of battle.enemyParty) { - if (enemyPokemon.isBoss()) { + // If the enemy pokemon is a boss and wasn't populated from data source, then set it up + if (enemyPokemon.isBoss() && !enemyPokemon.isPopulatedFromDataSource) { enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst))); enemyPokemon.initBattleInfo(); } @@ -871,7 +956,7 @@ export class EncounterPhase extends BattlePhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer ].flat(), + targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 + (enemyField.length) ? value + 300 : value - 300, duration: 2000, onComplete: () => { @@ -886,21 +971,21 @@ export class EncounterPhase extends BattlePhase { const enemyField = this.scene.getEnemyField(); if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - return i18next.t("battle:bossAppeared", {bossName: enemyField[0].name}); + return i18next.t("battle:bossAppeared", { bossName: getPokemonNameWithAffix(enemyField[0])}); } if (this.scene.currentBattle.battleType === BattleType.TRAINER) { if (this.scene.currentBattle.double) { - return i18next.t("battle:trainerAppearedDouble", {trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true)}); + return i18next.t("battle:trainerAppearedDouble", { trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }); } else { - return i18next.t("battle:trainerAppeared", {trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true)}); + return i18next.t("battle:trainerAppeared", { trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }); } } return enemyField.length === 1 - ? i18next.t("battle:singleWildAppeared", {pokemonName: enemyField[0].name}) - : i18next.t("battle:multiWildAppeared", {pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name}); + ? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].getNameToRender() }) + : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].getNameToRender(), pokemonName2: enemyField[1].getNameToRender() }); } doEncounterCommon(showEncounterMessage: boolean = true) { @@ -930,7 +1015,7 @@ export class EncounterPhase extends BattlePhase { this.scene.currentBattle.started = true; this.scene.playBgm(undefined); this.scene.pbTray.showPbTray(this.scene.getParty()); - this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); + this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); const doTrainerSummon = () => { this.hideEnemyTrainer(); const availablePartyMembers = this.scene.getEnemyParty().filter(p => !p.isFainted()).length; @@ -952,14 +1037,15 @@ export class EncounterPhase extends BattlePhase { if (!encounterMessages?.length) { doSummon(); } else { + let message: string; + this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); + const showDialogueAndSummon = () => { - let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex); - this.scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE,true), null, () => { + this.scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doSummon())); }); }; - if (this.scene.currentBattle.trainer.config.hasCharSprite) { + if (this.scene.currentBattle.trainer.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer.getKey(), getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon())); } else { showDialogueAndSummon(); @@ -978,7 +1064,21 @@ export class EncounterPhase extends BattlePhase { }); if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { - enemyField.map(p => this.scene.pushPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()))); + enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { + // if there is not a player party, we can't continue + if (!this.scene.getParty()?.length) { + return false; + } + // how many player pokemon are on the field ? + const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length; + // if it's a 2vs1, there will never be a 2nd pokemon on our field even + const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2); + // if it's a double, there should be 2, otherwise 1 + if (this.scene.currentBattle.double) { + return pokemonsOnFieldCount === requiredPokemonsOnField; + } + return pokemonsOnFieldCount === 1; + })); const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); @@ -986,7 +1086,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()); + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); if (!availablePartyMembers[0].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 0)); @@ -1016,7 +1116,6 @@ export class EncounterPhase extends BattlePhase { } } } - handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end()); } @@ -1041,6 +1140,10 @@ export class NextEncounterPhase extends EncounterPhase { super(scene); } + start() { + super.start(); + } + doEncounter(): void { this.scene.playBgm(undefined, true); @@ -1055,7 +1158,7 @@ export class NextEncounterPhase extends EncounterPhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer ].flat(), + targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -1098,7 +1201,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyField ].flat(), + targets: [this.scene.arenaEnemy, enemyField].flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -1120,6 +1223,9 @@ export class PostSummonPhase extends PokemonPhase { const pokemon = this.getPokemon(); + if (pokemon.status?.effect === StatusEffect.TOXIC) { + pokemon.status.turnCount = 0; + } this.scene.arena.applyTags(ArenaTrapTag, pokemon); applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); } @@ -1161,7 +1267,7 @@ export class SelectBiomePhase extends BattlePhase { let biomeChoices: Biome[]; this.scene.executeWithSeedOffset(() => { biomeChoices = (!Array.isArray(biomeLinks[currentBiome]) - ? [ biomeLinks[currentBiome] as Biome ] + ? [biomeLinks[currentBiome] as Biome] : biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) .filter((b, i) => !Array.isArray(b) || !Utils.randSeedInt(b[1])) .map(b => Array.isArray(b) ? b[0] : b); @@ -1216,7 +1322,7 @@ export class SwitchBiomePhase extends BattlePhase { } this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.lastEnemyTrainer ], + targets: [this.scene.arenaEnemy, this.scene.lastEnemyTrainer], x: "+=300", duration: 2000, onComplete: () => { @@ -1234,7 +1340,7 @@ export class SwitchBiomePhase extends BattlePhase { this.scene.arenaPlayerTransition.setVisible(true); this.scene.tweens.add({ - targets: [ this.scene.arenaPlayer, this.scene.arenaBgTransition, this.scene.arenaPlayerTransition ], + targets: [this.scene.arenaPlayer, this.scene.arenaBgTransition, this.scene.arenaPlayerTransition], duration: 1000, delay: 1000, ease: "Sine.easeInOut", @@ -1280,25 +1386,38 @@ export class SummonPhase extends PartyMemberPokemonPhase { */ preSummon(): void { const partyMember = this.getPokemon(); - // If the Pokemon about to be sent out is fainted, switch to the first non-fainted Pokemon - if (partyMember.isFainted()) { - console.warn("The Pokemon about to be sent out is fainted. Attempting to resolve..."); + // 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()) { + 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. + if (partyMember.isOnField()) { + partyMember.hideInfo(); + partyMember.setVisible(false); + this.scene.field.remove(partyMember); + this.scene.triggerPokemonFormChange(partyMember, SpeciesFormChangeActiveTrigger, true); + } + const party = this.getParty(); // Find the first non-fainted Pokemon index above the current one - const nonFaintedIndex = party.findIndex((p, i) => i > this.partyMemberIndex && !p.isFainted()); - if (nonFaintedIndex === -1) { + const legalIndex = party.findIndex((p, i) => i > this.partyMemberIndex && p.isAllowedInBattle()); + if (legalIndex === -1) { console.error("Party Details:\n", party); - throw new Error("All available Pokemon were fainted!"); + console.error("All available Pokemon were fainted or illegal!"); + this.scene.clearPhaseQueue(); + this.scene.unshiftPhase(new GameOverPhase(this.scene)); + this.end(); + return; } - // Swaps the fainted Pokemon and the first non-fainted Pokemon in the party - [party[this.partyMemberIndex], party[nonFaintedIndex]] = [party[nonFaintedIndex], party[this.partyMemberIndex]]; - console.warn("Swapped %s %O with %s %O", partyMember?.name, partyMember, party[0]?.name, party[0]); + // Swaps the fainted Pokemon and the first non-fainted legal Pokemon in the party + [party[this.partyMemberIndex], party[legalIndex]] = [party[legalIndex], party[this.partyMemberIndex]]; + console.warn("Swapped %s %O with %s %O", getPokemonNameWithAffix(partyMember), partyMember, getPokemonNameWithAffix(party[0]), party[0]); } if (this.player) { - this.scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: this.getPokemon().name })); + this.scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(this.getPokemon()) })); if (this.player) { this.scene.pbTray.hide(); } @@ -1318,7 +1437,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { this.scene.time.delayedCall(750, () => this.summon()); } else { const trainerName = this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); - const pokemonName = this.getPokemon().name; + const pokemonName = getPokemonNameWithAffix(this.getPokemon()); const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName }); this.scene.pbTrayEnemy.hide(); @@ -1337,7 +1456,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { if (this.fieldIndex === 1) { pokemon.setFieldPosition(FieldPosition.RIGHT, 0); } else { - const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length; + const availablePartyMembers = this.getParty().filter(p => p.isAllowedInBattle()).length; pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); } @@ -1415,7 +1534,6 @@ export class SummonPhase extends PartyMemberPokemonPhase { if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - this.queuePostSummon(); } } @@ -1438,6 +1556,15 @@ export class SwitchSummonPhase extends SummonPhase { private lastPokemon: Pokemon; + /** + * Constructor for creating a new SwitchSummonPhase + * @param scene {@linkcode BattleScene} the scene the phase is associated with + * @param fieldIndex integer representing position on the battle field + * @param slotIndex integer for the index of pokemon (in party of 6) to switch into + * @param doReturn boolean whether to render "comeback" dialogue + * @param batonPass boolean if the switch is from baton pass + * @param player boolean if the switch is from the player + */ constructor(scene: BattleScene, fieldIndex: integer, slotIndex: integer, doReturn: boolean, batonPass: boolean, player?: boolean) { super(scene, fieldIndex, player !== undefined ? player : true); @@ -1446,6 +1573,10 @@ export class SwitchSummonPhase extends SummonPhase { this.batonPass = batonPass; } + start(): void { + super.start(); + } + preSummon(): void { if (!this.player) { if (this.slotIndex === -1) { @@ -1473,10 +1604,10 @@ export class SwitchSummonPhase extends SummonPhase { } this.scene.ui.showText(this.player ? - i18next.t("battle:playerComeBack", { pokemonName: pokemon.name }) : + i18next.t("battle:playerComeBack", { pokemonName: getPokemonNameWithAffix(pokemon) }) : i18next.t("battle:trainerComeBack", { trainerName: this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER), - pokemonName: pokemon.name + pokemonName: getPokemonNameWithAffix(pokemon) }) ); this.scene.playSound("pb_rel"); @@ -1507,7 +1638,7 @@ export class SwitchSummonPhase extends SummonPhase { const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id) as SwitchEffectTransferModifier; if (batonPassModifier && !this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedPokemon.id)) { - this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedPokemon, false, false); + this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedPokemon, false); } } } @@ -1516,12 +1647,17 @@ export class SwitchSummonPhase extends SummonPhase { party[this.fieldIndex] = switchedPokemon; const showTextAndSummon = () => { this.scene.ui.showText(this.player ? - i18next.t("battle:playerGo", { pokemonName: switchedPokemon.name }) : + i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(switchedPokemon) }) : i18next.t("battle:trainerGo", { trainerName: this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER), - pokemonName: this.getPokemon().name + pokemonName: getPokemonNameWithAffix(this.getPokemon()) }) ); + // Ensure improperly persisted summon data (such as tags) is cleared upon switching + if (!this.batonPass) { + party[this.fieldIndex].resetBattleData(); + party[this.fieldIndex].resetSummonData(); + } this.summon(); }; if (this.player) { @@ -1542,11 +1678,16 @@ export class SwitchSummonPhase extends SummonPhase { super.onEnd(); const pokemon = this.getPokemon(); - const moveId = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.move?.move; + + const moveId = this.lastPokemon?.scene.currentBattle.lastMove; const lastUsedMove = moveId ? allMoves[moveId] : undefined; + const currentCommand = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command; + const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted(); + // Compensate for turn spent summoning - if (pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command === Command.POKEMON || !!lastUsedMove?.findAttr(attr => attr instanceof ForceSwitchOutAttr)) { //check if hard switch OR pivot move was used + // Or compensate for force switch move if switched out pokemon is not fainted + if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) { pokemon.battleSummonData.turnCount--; } @@ -1622,7 +1763,7 @@ export class ToggleDoublePositionPhase extends BattlePhase { const playerPokemon = this.scene.getPlayerField().find(p => p.isActive(true)); if (playerPokemon) { - playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => !p.isFainted()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { + playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { if (playerPokemon.getFieldIndex() === 1) { const party = this.scene.getParty(); party[1] = party[0]; @@ -1652,6 +1793,11 @@ export class CheckSwitchPhase extends BattlePhase { const pokemon = this.scene.getPlayerField()[this.fieldIndex]; + if (this.scene.battleStyle === BattleStyle.SET) { + super.end(); + return; + } + if (this.scene.field.getAll().indexOf(pokemon) === -1) { this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex)); super.end(); @@ -1668,7 +1814,7 @@ export class CheckSwitchPhase extends BattlePhase { return; } - this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? pokemon.name : i18next.t("battle:pokemon") }), null, () => { + this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); @@ -1688,7 +1834,7 @@ export class SummonMissingPhase extends SummonPhase { } preSummon(): void { - this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: this.getPokemon().name})); + this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: getPokemonNameWithAffix(this.getPokemon()) })); this.scene.time.delayedCall(250, () => this.summon()); } } @@ -1717,6 +1863,34 @@ export class TurnInitPhase extends FieldPhase { start() { super.start(); + this.scene.getPlayerField().forEach(p => { + // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch + if (p.isOnField() && !p.isAllowedInBattle()) { + this.scene.queueMessage(i18next.t("challenges:illegalEvolution", { "pokemon": p.name }), null, true); + + const allowedPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + + if (!allowedPokemon.length) { + // If there are no longer any legal pokemon in the party, game over. + this.scene.clearPhaseQueue(); + this.scene.unshiftPhase(new GameOverPhase(this.scene)); + } else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) { + // If there is at least one pokemon in the back that is legal to switch in, force a switch. + p.switchOut(false, true); + } else { + // If there are no pokemon in the back but we're not game overing, just hide the pokemon. + // This should only happen in double battles. + p.hideInfo(); + p.setVisible(false); + this.scene.field.remove(p); + this.scene.triggerPokemonFormChange(p, SpeciesFormChangeActiveTrigger, true); + } + if (allowedPokemon.length === 1 && this.scene.currentBattle.double) { + this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); + } + } + }); + //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); @@ -1751,9 +1925,15 @@ export class CommandPhase extends FieldPhase { super.start(); if (this.fieldIndex) { - const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1]; - if (allyCommand.command === Command.BALL || allyCommand.command === Command.RUN) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand.command, skip: true }; + // If we somehow are attempting to check the right pokemon but there's only one pokemon out + // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching + if (this.scene.getPlayerField().filter(p => p.isActive()).length === 1) { + this.fieldIndex = FieldPosition.CENTER; + } else { + const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1]; + if (allyCommand.command === Command.BALL || allyCommand.command === Command.RUN) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand.command, skip: true }; + } } } @@ -1767,7 +1947,7 @@ export class CommandPhase extends FieldPhase { while (moveQueue.length && moveQueue[0] && moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) - || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(playerPokemon, moveQueue[0].ignorePP))) { + || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(playerPokemon, moveQueue[0].ignorePP))) { moveQueue.shift(); } @@ -1797,15 +1977,18 @@ export class CommandPhase extends FieldPhase { case Command.FIGHT: let useStruggle = false; if (cursor === -1 || - playerPokemon.trySelectMove(cursor, args[0] as boolean) || - (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)) { + playerPokemon.trySelectMove(cursor, args[0] as boolean) || + (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)) { const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor].moveId : Moves.NONE : Moves.STRUGGLE; const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args }; const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2]; if (!moveId) { - turnCommand.targets = [ this.fieldIndex ]; + turnCommand.targets = [this.fieldIndex]; + } + console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); + if (moveTargets.targets.length > 1 && moveTargets.multiple) { + this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); } - console.log(moveTargets, playerPokemon.name); if (moveTargets.targets.length <= 1 || moveTargets.multiple) { turnCommand.move.targets = moveTargets.targets; } else if (playerPokemon.getTag(BattlerTagType.CHARGING) && playerPokemon.getMoveQueue().length >= 1) { @@ -1921,7 +2104,7 @@ export class CommandPhase extends FieldPhase { } this.scene.ui.showText( i18next.t("battle:noEscapePokemon", { - pokemonName: this.scene.getPokemonById(trapTag.sourceId).name, + pokemonName: getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)), moveName: trapTag.getMoveName(), escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") }), @@ -2024,7 +2207,7 @@ export class EnemyCommandPhase extends FieldPhase { const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores); battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.POKEMON, cursor: index, args: [ false ] }; + { command: Command.POKEMON, cursor: index, args: [false] }; battle.enemySwitchCounter++; @@ -2055,13 +2238,13 @@ export class SelectTargetPhase extends PokemonPhase { const turnCommand = this.scene.currentBattle.turnCommands[this.fieldIndex]; const move = turnCommand.move?.move; - this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (cursor: integer) => { + this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (targets: BattlerIndex[]) => { this.scene.ui.setMode(Mode.MESSAGE); - if (cursor === -1) { + if (targets.length < 1) { this.scene.currentBattle.turnCommands[this.fieldIndex] = null; this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); } else { - turnCommand.targets = [ cursor ]; + turnCommand.targets = targets; } if (turnCommand.command === Command.BALL && this.fieldIndex) { this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true; @@ -2086,6 +2269,7 @@ export class TurnStartPhase extends FieldPhase { this.scene.getField(true).filter(p => p.summonData).map(p => { const bypassSpeed = new Utils.BooleanHolder(false); + applyAbAttrs(BypassSpeedChanceAbAttr, p, null, bypassSpeed); this.scene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; }); @@ -2109,10 +2293,10 @@ export class TurnStartPhase extends FieldPhase { const aPriority = new Utils.IntegerHolder(aMove.priority); const bPriority = new Utils.IntegerHolder(bMove.priority); - applyMoveAttrs(IncrementMovePriorityAttr,this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a),null,aMove,aPriority); - applyMoveAttrs(IncrementMovePriorityAttr,this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b),null,bMove,bPriority); + applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority); + applyMoveAttrs(IncrementMovePriorityAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b), null, bMove, bPriority); - applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority); + applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === a), null, aMove, aPriority); applyAbAttrs(IncrementMovePriorityAbAttr, this.scene.getField().find(p => p?.isActive() && p.getBattlerIndex() === b), null, bMove, bPriority); if (aPriority.value !== bPriority.value) { @@ -2130,6 +2314,8 @@ export class TurnStartPhase extends FieldPhase { return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); + let orderIndex = 0; + for (const o of moveOrder) { const pokemon = field[o]; @@ -2142,6 +2328,7 @@ export class TurnStartPhase extends FieldPhase { switch (turnCommand.command) { case Command.FIGHT: const queuedMove = turnCommand.move; + pokemon.turnData.order = orderIndex++; if (!queuedMove) { continue; } @@ -2173,7 +2360,7 @@ export class TurnStartPhase extends FieldPhase { return; } }); - // if only one pokemon is alive, use that one + // if only one pokemon is alive, use that one if (playerActivePokemon.length > 1) { // find which active pokemon has faster speed const fasterPokemon = playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD) ? playerActivePokemon[0] : playerActivePokemon[1]; @@ -2188,9 +2375,7 @@ export class TurnStartPhase extends FieldPhase { } - if (this.scene.arena.weather) { - this.scene.pushPhase(new WeatherEffectPhase(this.scene, this.scene.arena.weather)); - } + this.scene.pushPhase(new WeatherEffectPhase(this.scene)); for (const o of order) { if (field[o].status && field[o].status.isPostTurn()) { @@ -2201,6 +2386,11 @@ export class TurnStartPhase extends FieldPhase { this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); + /** + * this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front + * of the queue and dequeues to start the next phase + * this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence + */ this.end(); } } @@ -2234,6 +2424,7 @@ export class BerryPhase extends FieldPhase { berryModifier.consumed = false; } } + this.scene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used } this.scene.updateModifiers(pokemon.isPlayer()); @@ -2262,7 +2453,7 @@ export class TurnEndPhase extends FieldPhase { pokemon.lapseTags(BattlerTagLapseType.TURN_END); if (pokemon.summonData.disabledMove && !--pokemon.summonData.disabledTurns) { - this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: `${getPokemonPrefix(pokemon)}${pokemon.name}`, moveName: allMoves[pokemon.summonData.disabledMove].name }))); + this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name }))); pokemon.summonData.disabledMove = Moves.NONE; } @@ -2270,7 +2461,7 @@ export class TurnEndPhase extends FieldPhase { if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { this.scene.unshiftPhase(new PokemonHealPhase(this.scene, pokemon.getBattlerIndex(), - Math.max(pokemon.getMaxHp() >> 4, 1), getPokemonMessage(pokemon, "'s HP was restored."), true)); + Math.max(pokemon.getMaxHp() >> 4, 1), i18next.t("battle:turnEndHpRestore", { pokemonName: getPokemonNameWithAffix(pokemon) }), true)); } if (!pokemon.isPlayer()) { @@ -2280,6 +2471,8 @@ export class TurnEndPhase extends FieldPhase { applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); + this.scene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); + this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); pokemon.battleSummonData.turnCount++; @@ -2327,7 +2520,7 @@ export class BattleEndPhase extends BattlePhase { } } - for (const pokemon of this.scene.getParty().filter(p => !p.isFainted())) { + for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); } @@ -2373,6 +2566,10 @@ export class CommonAnimPhase extends PokemonPhase { this.targetIndex = targetIndex; } + setAnimation(anim: CommonAnim) { + this.anim = anim; + } + start() { new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => { this.end(); @@ -2424,6 +2621,11 @@ export class MovePhase extends BattlePhase { if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) { this.scene.queueMessage(`${this.move.getName()} is disabled!`); } + if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails + this.fail(); + this.showMoveText(); + this.showFailedText(); + } return this.end(); } @@ -2443,22 +2645,36 @@ export class MovePhase extends BattlePhase { if (moveTarget) { const oldTarget = moveTarget.value; this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget)); + this.pokemon.getOpponents().forEach(p => { + const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag; + if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) { + moveTarget.value = p.getBattlerIndex(); + } + }); //Check if this move is immune to being redirected, and restore its target to the intended target if it is. - if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().getAttrs(BypassRedirectAttr).length)) { + if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) { //If an ability prevented this move from being redirected, display its ability pop up. - if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().getAttrs(BypassRedirectAttr).length) && oldTarget !== moveTarget.value) { + if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) { this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr))); } moveTarget.value = oldTarget; - } + } this.targets[0] = moveTarget.value; } + // Check for counterattack moves to switch target if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) { if (this.pokemon.turnData.attacksReceived.length) { - const attacker = this.pokemon.turnData.attacksReceived.length ? this.pokemon.scene.getPokemonById(this.pokemon.turnData.attacksReceived[0].sourceId) : null; - if (attacker?.isActive(true)) { - this.targets[0] = attacker.getBattlerIndex(); + const attack = this.pokemon.turnData.attacksReceived[0]; + this.targets[0] = attack.sourceBattlerIndex; + + // account for metal burst and comeuppance hitting remaining targets in double battles + // counterattack will redirect to remaining ally if original attacker faints + if (this.scene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) { + if (this.scene.getField()[this.targets[0]].hp === 0) { + const opposingField = this.pokemon.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField(); + this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex(); + } } } if (this.targets[0] === BattlerIndex.ATTACKER) { @@ -2536,7 +2752,7 @@ export class MovePhase extends BattlePhase { this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); } - if (!allMoves[this.move.moveId].getAttrs(CopyMoveAttr).length) { + if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { this.scene.currentBattle.lastMove = this.move.moveId; } @@ -2552,6 +2768,16 @@ export class MovePhase extends BattlePhase { failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain.terrainType); } } + + /** + * Trigger pokemon type change before playing the move animation + * Will still change the user's type when using Roar, Whirlwind, Trick-or-Treat, and Forest's Curse, + * regardless of whether the move successfully executes or not. + */ + if (success || [Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) { + applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); + } + if (success) { this.scene.unshiftPhase(this.getEffectPhase()); } else { @@ -2566,7 +2792,7 @@ export class MovePhase extends BattlePhase { this.scene.getPlayerField().forEach(pokemon => { applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); }); - this.scene.getEnemyParty().forEach(pokemon => { + this.scene.getEnemyField().forEach(pokemon => { applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); }); } @@ -2599,12 +2825,12 @@ export class MovePhase extends BattlePhase { } if (activated) { - this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectActivationText(this.pokemon.status.effect))); + this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1))); doMove(); } else { if (healed) { - this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectHealText(this.pokemon.status.effect))); + this.scene.queueMessage(getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); this.pokemon.resetStatus(); this.pokemon.updateInfo(); } @@ -2620,10 +2846,13 @@ export class MovePhase extends BattlePhase { } showMoveText(): void { - if (this.move.getMove().getAttrs(ChargeAttr).length) { + if (this.move.getMove().hasAttr(ChargeAttr)) { const lastMove = this.pokemon.getLastXMoves() as TurnMove[]; if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) { - this.scene.queueMessage(getPokemonMessage(this.pokemon, ` used\n${this.move.getName()}!`), 500); + this.scene.queueMessage(i18next.t("battle:useMove", { + pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), + moveName: this.move.getName() + }), 500); return; } } @@ -2632,7 +2861,10 @@ export class MovePhase extends BattlePhase { return; } - this.scene.queueMessage(getPokemonMessage(this.pokemon, ` used\n${this.move.getName()}!`), 500); + this.scene.queueMessage(i18next.t("battle:useMove", { + pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), + moveName: this.move.getName() + }), 500); applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents().find(() => true), this.move.getMove()); } @@ -2661,7 +2893,7 @@ export class MoveEffectPhase extends PokemonPhase { // of the left Pokemon and gets hit unless this is checked. if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { const i = targets.indexOf(battlerIndex); - targets.splice(i,i+1); + targets.splice(i, i + 1); } this.targets = targets; } @@ -2677,9 +2909,10 @@ export class MoveEffectPhase extends PokemonPhase { } const overridden = new Utils.BooleanHolder(false); + const move = this.move.getMove(); // Assume single target for override - applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget(), this.move.getMove(), overridden, this.move.virtual).then(() => { + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget(), move, overridden, this.move.virtual).then(() => { if (overridden.value) { return this.end(); @@ -2690,86 +2923,95 @@ export class MoveEffectPhase extends PokemonPhase { if (user.turnData.hitsLeft === undefined) { const hitCount = new Utils.IntegerHolder(1); // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getTarget(), this.move.getMove(), hitCount); - if (this.move.getMove() instanceof AttackMove && !this.move.getMove().getAttrs(FixedDamageAttr).length) { + applyMoveAttrs(MultiHitAttr, user, this.getTarget(), move, hitCount); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, targets.length, hitCount, new Utils.IntegerHolder(0)); + if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); } user.turnData.hitsLeft = user.turnData.hitCount = hitCount.value; } const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; - user.pushMoveHistory(moveHistoryEntry); - const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); + const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)])); const activeTargets = targets.map(t => t.isActive(true)); - if (!activeTargets.length || (!this.move.getMove().getAttrs(VariableTargetAttr).length && !this.move.getMove().isMultiTarget() && !targetHitChecks[this.targets[0]])) { - user.turnData.hitCount = 1; - user.turnData.hitsLeft = 1; + if (!activeTargets.length || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) { + this.stopMultiHit(); if (activeTargets.length) { - this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!")); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(this.getTarget()) })); moveHistoryEntry.result = MoveResult.MISS; - applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); + applyMoveAttrs(MissEffectAttr, user, null, move); } else { this.scene.queueMessage(i18next.t("battle:attackFailed")); moveHistoryEntry.result = MoveResult.FAIL; } + user.pushMoveHistory(moveHistoryEntry); return this.end(); } const applyAttrs: Promise[] = []; // Move animation only needs one target - new MoveAnim(this.move.getMove().id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => { + new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => { for (const target of targets) { if (!targetHitChecks[target.getBattlerIndex()]) { - user.turnData.hitCount = 1; - user.turnData.hitsLeft = 1; - this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!")); + this.stopMultiHit(target); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); if (moveHistoryEntry.result === MoveResult.PENDING) { moveHistoryEntry.result = MoveResult.MISS; } - applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); + user.pushMoveHistory(moveHistoryEntry); + applyMoveAttrs(MissEffectAttr, user, null, move); continue; } - const isProtected = !this.move.getMove().hasFlag(MoveFlags.IGNORE_PROTECT) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)); + const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)); - const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS; + const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); + const firstTarget = (moveHistoryEntry.result === MoveResult.PENDING); + + if (firstHit) { + user.pushMoveHistory(moveHistoryEntry); + } moveHistoryEntry.result = MoveResult.SUCCESS; - const hitResult = !isProtected ? target.apply(user, this.move) : HitResult.NO_EFFECT; + const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; - this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); + const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); + + if (lastHit) { + this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); + } applyAttrs.push(new Promise(resolve => { - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit), - user, target, this.move.getMove()).then(() => { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), + user, target, move).then(() => { if (hitResult !== HitResult.FAIL) { - const chargeEffect = !!this.move.getMove().getAttrs(ChargeAttr).find(ca => (ca as ChargeAttr).usedChargeEffect(user, this.getTarget(), this.move.getMove())); + const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move)); // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present - Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && (attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit), user, target, this.move.getMove())).then(() => { + Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY + && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { if (hitResult !== HitResult.NO_EFFECT) { applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit), user, target, this.move.getMove()).then(() => { - if (hitResult < HitResult.NO_EFFECT) { + && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { + if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { const flinched = new Utils.BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); } } - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit), - user, target, this.move.getMove()).then(() => { - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, hitResult).then(() => { + Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT + && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { + return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); } })).then(() => { - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move, hitResult).then(() => { + applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); } resolve(); }); @@ -2778,7 +3020,7 @@ export class MoveEffectPhase extends PokemonPhase { ).then(() => resolve()); }); } else { - applyMoveAttrs(NoEffectAttr, user, null, this.move.getMove()).then(() => resolve()); + applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); } }); } else { @@ -2787,14 +3029,17 @@ export class MoveEffectPhase extends PokemonPhase { }); })); } - // Trigger effect which should only apply one time after all targeted effects have already applied - const postTarget = applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_TARGET, - user, null, this.move.getMove()); + // Trigger effect which should only apply one time on the last hit after all targeted effects have already applied + const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : + null; - if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after - applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); - } else { // Otherwise, push a new asynchronous move effect - applyAttrs.push(postTarget); + if (!!postTarget) { + if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after + applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); + } else { // Otherwise, push a new asynchronous move effect + applyAttrs.push(postTarget); + } } Promise.allSettled(applyAttrs).then(() => this.end()); @@ -2803,13 +3048,18 @@ export class MoveEffectPhase extends PokemonPhase { } end() { + const move = this.move.getMove(); + move.type = move.defaultType; const user = this.getUserPokemon(); if (user) { if (--user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); } else { + // Queue message for number of hits made by multi-move + // If multi-hit attack only hits once, still want to render a message const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0); - if (hitsTotal > 1) { + if (hitsTotal > 1 || user.turnData.hitsLeft > 0) { + // If there are multiple hits, or if there are hits of the multi-hit move left this.scene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); } this.scene.applyModifiers(HitHealModifier, this.player, user); @@ -2827,9 +3077,13 @@ export class MoveEffectPhase extends PokemonPhase { const user = this.getUserPokemon(); - // Hit check only calculated on first hit for multi-hit moves + // Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits. + // However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal + // multi-hit move and proceed with all hits if (user.turnData.hitsLeft < user.turnData.hitCount) { - return true; + if (!this.move.getMove().hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) { + return true; + } } if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { @@ -2837,62 +3091,29 @@ export class MoveEffectPhase extends PokemonPhase { } // If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match - if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().slice(1).find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { return true; } - const hiddenTag = target.getTag(HiddenTag); - if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).filter(hta => (hta as HitsTagAttr).tagType === hiddenTag.tagType).length) { + if (target.getTag(BattlerTagType.ALWAYS_GET_HIT)) { + return true; + } + + const hiddenTag = target.getTag(SemiInvulnerableTag); + if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === hiddenTag.tagType)) { return false; } - const moveAccuracy = new Utils.NumberHolder(this.move.getMove().accuracy); + const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user, target); - applyMoveAttrs(VariableAccuracyAttr, user, target, this.move.getMove(), moveAccuracy); - - if (moveAccuracy.value === -1) { + if (moveAccuracy === -1) { return true; } - const isOhko = !!this.move.getMove().getAttrs(OneHitKOAccuracyAttr).length; - - if (!isOhko) { - user.scene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); - } - - if (this.scene.arena.weather?.weatherType === WeatherType.FOG) { - moveAccuracy.value = Math.floor(moveAccuracy.value * 0.9); - } - - if (!isOhko && this.scene.arena.getTag(ArenaTagType.GRAVITY)) { - moveAccuracy.value = Math.floor(moveAccuracy.value * 1.67); - } - - const userAccuracyLevel = new Utils.IntegerHolder(user.summonData.battleStats[BattleStat.ACC]); - const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccuracyLevel); - applyAbAttrs(IgnoreOpponentStatChangesAbAttr, user, null, targetEvasionLevel); - applyAbAttrs(IgnoreOpponentEvasionAbAttr, user, null, targetEvasionLevel); - applyMoveAttrs(IgnoreOpponentStatChangesAttr, user, target, this.move.getMove(), targetEvasionLevel); - this.scene.applyModifiers(TempBattleStatBoosterModifier, this.player, TempBattleStat.ACC, userAccuracyLevel); - + const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove()); const rand = user.randSeedInt(100, 1); - const accuracyMultiplier = new Utils.NumberHolder(1); - if (userAccuracyLevel.value !== targetEvasionLevel.value) { - accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value - ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 - : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); - } - - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, user, BattleStat.ACC, accuracyMultiplier, this.move.getMove()); - - const evasionMultiplier = new Utils.NumberHolder(1); - applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this.getTarget(), BattleStat.EVA, evasionMultiplier); - - accuracyMultiplier.value /= evasionMultiplier.value; - - return rand <= moveAccuracy.value * accuracyMultiplier.value; + return rand <= moveAccuracy * accuracyMultiplier; } getUserPokemon(): Pokemon { @@ -2910,6 +3131,28 @@ export class MoveEffectPhase extends PokemonPhase { return this.getTargets().find(() => true); } + removeTarget(target: Pokemon): void { + const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); + if (targetIndex !== -1) { + this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); + } + } + + stopMultiHit(target?: Pokemon): void { + /** If given a specific target, remove the target from subsequent strikes */ + if (target) { + this.removeTarget(target); + } + /** + * If no target specified, or the specified target was the last of this move's + * targets, completely cancel all subsequent strikes. + */ + if (!target || this.targets.length === 0 ) { + this.getUserPokemon().turnData.hitCount = 1; + this.getUserPokemon().turnData.hitsLeft = 1; + } + } + getNewHitPhase() { return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); } @@ -2958,7 +3201,7 @@ export class MoveAnimTestPhase extends BattlePhase { } initMoveAnim(this.scene, moveId).then(() => { - loadMoveAnimAssets(this.scene, [ moveId ], true) + loadMoveAnimAssets(this.scene, [moveId], true) .then(() => { new MoveAnim(moveId, player ? this.scene.getPlayerPokemon() : this.scene.getEnemyPokemon(), (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon()).getBattlerIndex()).play(this.scene, () => { if (player) { @@ -2984,12 +3227,19 @@ export class ShowAbilityPhase extends PokemonPhase { start() { super.start(); - this.scene.abilityBar.showAbility(this.getPokemon(), this.passive); + const pokemon = this.getPokemon(); + + this.scene.abilityBar.showAbility(pokemon, this.passive); + if (pokemon.battleData) { + pokemon.battleData.abilityRevealed = true; + } this.end(); } } +export type StatChangeCallback = (target: Pokemon, changed: BattleStat[], relativeChanges: number[]) => void; + export class StatChangePhase extends PokemonPhase { private stats: BattleStat[]; private selfTarget: boolean; @@ -2997,8 +3247,10 @@ export class StatChangePhase extends PokemonPhase { private showMessage: boolean; private ignoreAbilities: boolean; private canBeCopied: boolean; + private onChange: StatChangeCallback; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true) { + + constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback = null) { super(scene, battlerIndex); this.selfTarget = selfTarget; @@ -3007,6 +3259,7 @@ export class StatChangePhase extends PokemonPhase { this.showMessage = showMessage; this.ignoreAbilities = ignoreAbilities; this.canBeCopied = canBeCopied; + this.onChange = onChange; } start() { @@ -3048,6 +3301,8 @@ export class StatChangePhase extends PokemonPhase { const battleStats = this.getPokemon().summonData.battleStats; const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]); + this.onChange?.(this.getPokemon(), filteredStats, relLevels); + const end = () => { if (this.showMessage) { const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels); @@ -3068,6 +3323,21 @@ export class StatChangePhase extends PokemonPhase { applyPostStatChangeAbAttrs(PostStatChangeAbAttr, pokemon, filteredStats, this.levels, this.selfTarget); + // Look for any other stat change phases; if this is the last one, do White Herb check + const existingPhase = this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex); + if (!(existingPhase instanceof StatChangePhase)) { + // Apply White Herb if needed + const whiteHerb = this.scene.applyModifier(PokemonResetNegativeStatStageModifier, this.player, pokemon) as PokemonResetNegativeStatStageModifier; + // If the White Herb was applied, consume it + if (whiteHerb) { + --whiteHerb.stackCount; + if (whiteHerb.stackCount <= 0) { + this.scene.removeModifier(whiteHerb); + } + this.scene.updateModifiers(this.player); + } + } + pokemon.updateInfo(); handleTutorial(this.scene, Tutorial.Stat_Change).then(() => super.end()); @@ -3082,7 +3352,10 @@ export class StatChangePhase extends PokemonPhase { const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); - const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", filteredStats.length > 1 ? "mix" : BattleStat[filteredStats[0]].toLowerCase()); + // On increase, show the red sprite located at ATK + // On decrease, show the blue sprite located at SPD + const spriteColor = levels.value >= 1 ? BattleStat[BattleStat.ATK].toLowerCase() : BattleStat[BattleStat.SPD].toLowerCase(); + const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", spriteColor); statSprite.setPipeline(this.scene.fieldSpritePipeline); statSprite.setAlpha(0); statSprite.setScale(6); @@ -3127,7 +3400,7 @@ export class StatChangePhase extends PokemonPhase { } aggregateStatChanges(random: boolean = false): void { - const isAccEva = [ BattleStat.ACC, BattleStat.EVA ].some(s => this.stats.includes(s)); + const isAccEva = [BattleStat.ACC, BattleStat.EVA].some(s => this.stats.includes(s)); let existingPhase: StatChangePhase; if (this.stats.length === 1) { while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.stats.length === 1 @@ -3147,7 +3420,7 @@ export class StatChangePhase extends PokemonPhase { } } while ((existingPhase = (this.scene.findPhase(p => p instanceof StatChangePhase && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget - && ([ BattleStat.ACC, BattleStat.EVA ].some(s => p.stats.includes(s)) === isAccEva) + && ([BattleStat.ACC, BattleStat.EVA].some(s => p.stats.includes(s)) === isAccEva) && p.levels === this.levels && p.showMessage === this.showMessage && p.ignoreAbilities === this.ignoreAbilities) as StatChangePhase))) { this.stats.push(...existingPhase.stats); if (!this.scene.tryRemovePhase(p => p === existingPhase)) { @@ -3174,12 +3447,13 @@ export class StatChangePhase extends PokemonPhase { if (relLevelStats.length > 1) { statsFragment = relLevelStats.length >= 5 - ? "stats" - : `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} and ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`; + ? i18next.t("battle:stats") + : `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} ${i18next.t("battle:statsAnd")} ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`; + messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1,relLevelStats.length)); } else { statsFragment = getBattleStatName(relLevelStats[0]); + messages.push(getBattleStatLevelChangeDescription(getPokemonNameWithAffix(this.getPokemon()), statsFragment, Math.abs(parseInt(rl)), levels >= 1,relLevelStats.length)); } - messages.push(getPokemonMessage(this.getPokemon(), `'s ${statsFragment} ${getBattleStatLevelChangeDescription(Math.abs(parseInt(rl)), levels >= 1)}!`)); }); return messages; @@ -3189,12 +3463,22 @@ export class StatChangePhase extends PokemonPhase { export class WeatherEffectPhase extends CommonAnimPhase { public weather: Weather; - constructor(scene: BattleScene, weather: Weather) { - super(scene, undefined, undefined, CommonAnim.SUNNY + (weather.weatherType - 1)); - this.weather = weather; + constructor(scene: BattleScene) { + super(scene, undefined, undefined, CommonAnim.SUNNY + ((scene?.arena?.weather?.weatherType || WeatherType.NONE) - 1)); + this.weather = scene?.arena?.weather; } start() { + // Update weather state with any changes that occurred during the turn + this.weather = this.scene?.arena?.weather; + + if (!this.weather) { + this.end(); + return; + } + + this.setAnimation(CommonAnim.SUNNY + (this.weather.weatherType - 1)); + if (this.weather.isDamaging()) { const cancelled = new Utils.BooleanHolder(false); @@ -3259,7 +3543,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { } pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(this.scene, () => { - this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectObtainText(this.statusEffect, this.sourceText))); + this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText)); if (pokemon.status.isPostTurn()) { this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); } @@ -3268,7 +3552,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { return; } } else if (pokemon.status.effect === this.statusEffect) { - this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectOverlapText(this.statusEffect))); + this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon))); } this.end(); } @@ -3285,9 +3569,10 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { pokemon.status.incrementTurn(); const cancelled = new Utils.BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); + applyAbAttrs(BlockStatusDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectActivationText(pokemon.status.effect))); + this.scene.queueMessage(getStatusEffectActivationText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); let damage: integer = 0; switch (pokemon.status.effect) { case StatusEffect.POISON: @@ -3301,7 +3586,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { break; } if (damage) { - // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... + // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage, false, true)); pokemon.updateInfo(); } @@ -3313,6 +3598,14 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { this.end(); } } + + override end() { + if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { + this.scene.initFinalBossPhaseTwo(this.getPokemon()); + } else { + super.end(); + } + } } export class MessagePhase extends Phase { @@ -3368,7 +3661,9 @@ export class DamagePhase extends PokemonPhase { super.start(); if (this.damageResult === HitResult.ONE_HIT_KO) { - this.scene.toggleInvert(true); + if (this.scene.moveAnimations) { + this.scene.toggleInvert(true); + } this.scene.time.delayedCall(Utils.fixedInt(1000), () => { this.scene.toggleInvert(false); this.applyDamage(); @@ -3418,34 +3713,12 @@ export class DamagePhase extends PokemonPhase { } } - end() { - switch (this.scene.currentBattle.battleSpec) { - case BattleSpec.FINAL_BOSS: - const pokemon = this.getPokemon(); - if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) { - this.scene.fadeOutBgm(Utils.fixedInt(2000), false); - this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, null, () => { - this.scene.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true); - pokemon.generateAndPopulateMoveset(1); - this.scene.setFieldScale(0.75); - this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); - this.scene.currentBattle.double = true; - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()); - if (availablePartyMembers.length > 1) { - this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true)); - if (!availablePartyMembers[1].isOnField()) { - this.scene.pushPhase(new SummonPhase(this.scene, 1)); - } - } - - super.end(); - }); - return; - } - break; + override end() { + if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { + this.scene.initFinalBossPhaseTwo(this.getPokemon()); + } else { + super.end(); } - - super.end(); } } @@ -3481,11 +3754,18 @@ export class FaintPhase extends PokemonPhase { doFaint(): void { const pokemon = this.getPokemon(); - this.scene.queueMessage(getPokemonMessage(pokemon, " fainted!"), null, true); + // Track total times pokemon have been KO'd for supreme overlord/last respects + if (pokemon.isPlayer()) { + this.scene.currentBattle.playerFaints += 1; + } else { + this.scene.currentBattle.enemyFaints += 1; + } + + this.scene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true); if (pokemon.turnData?.attacksReceived?.length) { const lastAttack = pokemon.turnData.attacksReceived[0]; - applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, this.scene.getPokemonById(lastAttack.sourceId), new PokemonMove(lastAttack.move), lastAttack.result); + applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, this.scene.getPokemonById(lastAttack.sourceId), new PokemonMove(lastAttack.move).getMove(), lastAttack.result); } const alivePlayField = this.scene.getField(true); @@ -3495,7 +3775,7 @@ export class FaintPhase extends PokemonPhase { if (defeatSource?.isOnField()) { applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; - const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr) as PostVictoryStatChangeAttr[]; + const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr); if (pvattrs.length) { for (const pvattr of pvattrs) { pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); @@ -3505,11 +3785,11 @@ export class FaintPhase extends PokemonPhase { } if (this.player) { - const nonFaintedPartyMembers = this.scene.getParty().filter(p => !p.isFainted()); - const nonFaintedPartyMemberCount = nonFaintedPartyMembers.length; + const nonFaintedLegalPartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const nonFaintedPartyMemberCount = nonFaintedLegalPartyMembers.length; if (!nonFaintedPartyMemberCount) { this.scene.unshiftPhase(new GameOverPhase(this.scene)); - } else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedPartyMembers[0].isActive(true))) { + } else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedLegalPartyMembers[0].isActive(true))) { this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); } if (nonFaintedPartyMemberCount === 1 && this.scene.currentBattle.double) { @@ -3643,7 +3923,9 @@ export class VictoryPhase extends PokemonPhase { expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; } const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); - this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + const modifierBonusExp = new Utils.NumberHolder(1); + this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, modifierBonusExp); + pokemonExp.value *= modifierBonusExp.value; partyMemberExp.push(Math.floor(pokemonExp.value)); } @@ -3744,27 +4026,24 @@ export class TrainerVictoryPhase extends BattlePhase { const trainerType = this.scene.currentBattle.trainer.config.trainerType; if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); } } this.scene.ui.showText(i18next.t("battle:trainerDefeated", { trainerName: this.scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }), null, () => { const victoryMessages = this.scene.currentBattle.trainer.getVictoryMessages(); - const showMessage = () => { - let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(victoryMessages), this.scene.currentBattle.waveIndex); - const messagePages = message.split(/\$/g).map(m => m.trim()); + let message: string; + this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(victoryMessages), this.scene.currentBattle.waveIndex); - for (let p = messagePages.length - 1; p >= 0; p--) { - const originalFunc = showMessageOrEnd; - showMessageOrEnd = () => this.scene.ui.showDialogue(messagePages[p], this.scene.currentBattle.trainer.getName(), null, originalFunc); - } + const showMessage = () => { + const originalFunc = showMessageOrEnd; + showMessageOrEnd = () => this.scene.ui.showDialogue(message, this.scene.currentBattle.trainer.getName(), null, originalFunc); showMessageOrEnd(); }; let showMessageOrEnd = () => this.end(); if (victoryMessages?.length) { - if (this.scene.currentBattle.trainer.config.hasCharSprite) { + if (this.scene.currentBattle.trainer.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { const originalFunc = showMessageOrEnd; showMessageOrEnd = () => this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => originalFunc())); this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(this.scene.currentBattle.trainer.getKey(), getCharVariantFromDialogue(victoryMessages[0])).then(() => showMessage())); @@ -3794,6 +4073,10 @@ export class MoneyRewardPhase extends BattlePhase { this.scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); + if (this.scene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { + moneyAmount.value *= 2; + } + this.scene.addMoney(moneyAmount.value); const userLocale = navigator.language || "en-US"; @@ -3824,7 +4107,7 @@ export class ModifierRewardPhase extends BattlePhase { const newModifier = this.modifierType.newModifier(); this.scene.addModifier(newModifier).then(() => { this.scene.playSound("item_fanfare"); - this.scene.ui.showText(`You received\n${newModifier.type.name}!`, null, () => resolve(), null, true); + this.scene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier.type.name }), null, () => resolve(), null, true); }); }); } @@ -3842,7 +4125,7 @@ export class GameOverModifierRewardPhase extends ModifierRewardPhase { this.scene.playSound("level_up_fanfare"); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.fadeIn(250).then(() => { - this.scene.ui.showText(`You received\n${newModifier.type.name}!`, null, () => { + this.scene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier.type.name }), null, () => { this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); resolve(); }, null, true, 1500); @@ -3867,7 +4150,11 @@ export class RibbonModifierRewardPhase extends ModifierRewardPhase { this.scene.addModifier(newModifier).then(() => { this.scene.playSound("level_up_fanfare"); this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(`${this.species.name} beat ${this.scene.gameMode.getName()} Mode for the first time!\nYou received ${newModifier.type.name}!`, null, () => { + this.scene.ui.showText(i18next.t("battle:beatModeFirstTime", { + speciesName: this.species.name, + gameMode: this.scene.gameMode.getName(), + newModifier: newModifier.type.name + }), null, () => { resolve(); }, null, true, 1500); }); @@ -3898,7 +4185,7 @@ export class GameOverPhase extends BattlePhase { } else if (this.victory || !this.scene.enableRetries) { this.handleGameOver(); } else { - this.scene.ui.showText("Would you like to retry from the start of the battle?", null, () => { + this.scene.ui.showText(i18next.t("battle:retryBattle"), null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.fadeOut(1250).then(() => { this.scene.reset(); @@ -3906,7 +4193,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.pushPhase(new EncounterPhase(this.scene, true)); - const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()).length; + const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length; this.scene.pushPhase(new SummonPhase(this.scene, 0)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -3948,7 +4235,6 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } - this.scene.gameData.getSession(this.scene.sessionSlotId).then(sessionData => { if (sessionData) { this.scene.gameData.saveRunHistory(this.scene, sessionData, this.victory, true); @@ -3956,7 +4242,6 @@ export class GameOverPhase extends BattlePhase { }).catch(err => { console.error(err); }); - const fadeDuration = this.victory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); @@ -3967,6 +4252,10 @@ export class GameOverPhase extends BattlePhase { this.scene.clearPhaseQueue(); this.scene.ui.clearText(); + if (this.victory && this.scene.gameMode.isChallenge) { + this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c)); + } + const clear = (endCardPhase?: EndCardPhase) => { if (newClear) { this.handleUnlocks(); @@ -3984,19 +4273,27 @@ export class GameOverPhase extends BattlePhase { }; if (this.victory && this.scene.gameMode.isClassic) { - this.scene.ui.fadeIn(500).then(() => { - this.scene.charSprite.showCharacter(`rival_${this.scene.gameData.gender === PlayerGender.FEMALE ? "m" : "f"}`, getCharVariantFromDialogue(miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1])).then(() => { - this.scene.ui.showDialogue(miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1], this.scene.gameData.gender === PlayerGender.FEMALE ? trainerConfigs[TrainerType.RIVAL].name : trainerConfigs[TrainerType.RIVAL].nameFemale, null, () => { - this.scene.ui.fadeOut(500).then(() => { - this.scene.charSprite.hide().then(() => { - const endCardPhase = new EndCardPhase(this.scene); - this.scene.unshiftPhase(endCardPhase); - clear(endCardPhase); + const message = miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1]; + + if (!this.scene.ui.shouldSkipDialogue(message)) { + this.scene.ui.fadeIn(500).then(() => { + this.scene.charSprite.showCharacter(`rival_${this.scene.gameData.gender === PlayerGender.FEMALE ? "m" : "f"}`, getCharVariantFromDialogue(miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1])).then(() => { + this.scene.ui.showDialogue(message, this.scene.gameData.gender === PlayerGender.FEMALE ? trainerConfigs[TrainerType.RIVAL].name : trainerConfigs[TrainerType.RIVAL].nameFemale, null, () => { + this.scene.ui.fadeOut(500).then(() => { + this.scene.charSprite.hide().then(() => { + const endCardPhase = new EndCardPhase(this.scene); + this.scene.unshiftPhase(endCardPhase); + clear(endCardPhase); + }); }); }); }); }); - }); + } else { + const endCardPhase = new EndCardPhase(this.scene); + this.scene.unshiftPhase(endCardPhase); + clear(endCardPhase); + } } else { clear(); } @@ -4009,7 +4306,7 @@ export class GameOverPhase extends BattlePhase { If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */ if (this.victory) { if (!Utils.isLocal) { - Utils.apiFetch(`savedata/newclear?slot=${this.scene.sessionSlotId}`, true) + Utils.apiFetch(`savedata/session/newclear?slot=${this.scene.sessionSlotId}&clientSessionId=${clientSessionId}`, true) .then(response => response.json()) .then(newClear => doGameOver(newClear)); } else { @@ -4065,7 +4362,7 @@ export class EndCardPhase extends Phase { this.endCard.setScale(0.5); this.scene.field.add(this.endCard); - this.text = addTextObject(this.scene, this.scene.game.canvas.width / 12, (this.scene.game.canvas.height / 6) - 16, "Congratulations!", TextStyle.SUMMARY, { fontSize: "128px" }); + this.text = addTextObject(this.scene, this.scene.game.canvas.width / 12, (this.scene.game.canvas.height / 6) - 16, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" }); this.text.setOrigin(0.5); this.scene.field.add(this.text); @@ -4095,7 +4392,7 @@ export class UnlockPhase extends Phase { this.scene.gameData.unlocks[this.unlockable] = true; this.scene.playSound("level_up_fanfare"); this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(`${getUnlockableName(this.unlockable)}\nhas been unlocked.`, null, () => { + this.scene.ui.showText(i18next.t("battle:unlockedSomething", { unlockedThing: getUnlockableName(this.unlockable) }), null, () => { this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); this.end(); }, null, true, 1500); @@ -4162,17 +4459,17 @@ export class SwitchPhase extends BattlePhase { super.start(); // Skip modal switch if impossible - if (this.isModal && !this.scene.getParty().filter(p => !p.isFainted() && !p.isActive(true)).length) { + if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { return super.end(); } // Check if there is any space still in field - if (this.isModal && this.scene.getPlayerField().filter(p => !p.isFainted() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) { + if (this.isModal && this.scene.getPlayerField().filter(p => p.isAllowedInBattle() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) { return super.end(); } - // Override field index to 0 in case of double battle where 2/3 remaining party members fainted at once - const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => !p.isFainted()).length > 1 ? this.fieldIndex : 0; + // Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once + const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0; this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { @@ -4199,7 +4496,7 @@ export class ExpPhase extends PlayerPartyMemberPokemonPhase { const exp = new Utils.NumberHolder(this.expValue); this.scene.applyModifiers(ExpBoosterModifier, true, exp); exp.value = Math.floor(exp.value); - this.scene.ui.showText(i18next.t("battle:expGain", { pokemonName: pokemon.name, exp: exp.value }), null, () => { + this.scene.ui.showText(i18next.t("battle:expGain", { pokemonName: getPokemonNameWithAffix(pokemon), exp: exp.value }), null, () => { const lastLevel = pokemon.level; pokemon.addExp(exp.value); const newLevel = pokemon.level; @@ -4237,20 +4534,20 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { this.scene.unshiftPhase(new HidePartyExpBarPhase(this.scene)); pokemon.updateInfo(); - if (this.scene.expParty === 2) { // 2 - Skip - no level up frame nor message + if (this.scene.expParty === ExpNotification.SKIP) { this.end(); - } else if (this.scene.expParty === 1) { // 1 - Only level up - we display the level up in the small frame instead of a message + } else if (this.scene.expParty === ExpNotification.ONLY_LEVEL_UP) { if (newLevel > lastLevel) { // this means if we level up // instead of displaying the exp gain in the small frame, we display the new level // we use the same method for mode 0 & 1, by giving a parameter saying to display the exp or the level - this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, newLevel).then(() => { + this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === ExpNotification.ONLY_LEVEL_UP, newLevel).then(() => { setTimeout(() => this.end(), 800 / Math.pow(2, this.scene.expGainsSpeed)); }); } else { this.end(); } } else if (this.scene.expGainsSpeed < 3) { - this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, newLevel).then(() => { + this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, false, newLevel).then(() => { setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed)); }); } else { @@ -4297,16 +4594,16 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { const prevStats = pokemon.stats.slice(0); pokemon.calculateStats(); pokemon.updateInfo(); - if (this.scene.expParty === 0) { // 0 - default - the normal exp gain display, nothing changed + if (this.scene.expParty === ExpNotification.DEFAULT) { this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: this.getPokemon().name, level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); - } else if (this.scene.expParty === 2) { // 2 - Skip - no level up frame nor message + this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.getPokemon()), level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); + } else if (this.scene.expParty === ExpNotification.SKIP) { this.end(); - } else { // 1 - Only level up - we display the level up in the small frame instead of a message + } else { // we still want to display the stats if activated this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()); } - if (this.level <= 100) { + if (this.lastLevel < 100) { // this feels like an unnecessary optimization const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); for (const lm of levelMoves) { this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1])); @@ -4353,11 +4650,11 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { if (emptyMoveIndex > -1) { pokemon.setMove(emptyMoveIndex, this.moveId); initMoveAnim(this.scene, this.moveId).then(() => { - loadMoveAnimAssets(this.scene, [ this.moveId ], true) + loadMoveAnimAssets(this.scene, [this.moveId], true) .then(() => { this.scene.ui.setMode(messageMode).then(() => { this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: pokemon.name, moveName: move.name }), null, () => { + this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true); this.end(); }, messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true); @@ -4366,15 +4663,15 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { }); } else { this.scene.ui.setMode(messageMode).then(() => { - this.scene.ui.showText(i18next.t("battle:learnMovePrompt", { pokemonName: pokemon.name, moveName: move.name }), null, () => { - this.scene.ui.showText(i18next.t("battle:learnMoveLimitReached", { pokemonName: pokemon.name }), null, () => { + this.scene.ui.showText(i18next.t("battle:learnMovePrompt", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => { + this.scene.ui.showText(i18next.t("battle:learnMoveLimitReached", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { this.scene.ui.showText(i18next.t("battle:learnMoveReplaceQuestion", { moveName: move.name }), null, () => { const noHandler = () => { this.scene.ui.setMode(messageMode).then(() => { this.scene.ui.showText(i18next.t("battle:learnMoveStopTeaching", { moveName: move.name }), null, () => { this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => { this.scene.ui.setMode(messageMode); - this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: pokemon.name, moveName: move.name }), null, () => this.end(), null, true); + this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => this.end(), null, true); }, () => { this.scene.ui.setMode(messageMode); this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId)); @@ -4393,7 +4690,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } this.scene.ui.setMode(messageMode).then(() => { this.scene.ui.showText(i18next.t("battle:countdownPoof"), null, () => { - this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: pokemon.name, moveName: pokemon.moveset[moveIndex].getName() }), null, () => { + this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex].getName() }), null, () => { this.scene.ui.showText(i18next.t("battle:learnMoveAnd"), null, () => { pokemon.setMove(moveIndex, Moves.NONE); this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId)); @@ -4435,7 +4732,7 @@ export class PokemonHealPhase extends CommonAnimPhase { } start() { - if (!this.skipAnim && (this.revive || this.getPokemon().hp) && this.getPokemon().getHpRatio() < 1) { + if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) { super.start(); } else { this.end(); @@ -4450,12 +4747,11 @@ export class PokemonHealPhase extends CommonAnimPhase { return; } - const fullHp = pokemon.getHpRatio() >= 1; - const hasMessage = !!this.message; + const healOrDamage = (!pokemon.isFullHp() || this.hpHealed < 0); let lastStatusEffect = StatusEffect.NONE; - if (!fullHp || this.hpHealed < 0) { + if (healOrDamage) { const hpRestoreMultiplier = new Utils.IntegerHolder(1); if (!this.revive) { this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); @@ -4489,7 +4785,7 @@ export class PokemonHealPhase extends CommonAnimPhase { pokemon.resetStatus(); pokemon.updateInfo().then(() => super.end()); } else if (this.showFullHpMessage) { - this.message = getPokemonMessage(pokemon, "'s\nHP is full!"); + this.message = i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(pokemon) }); } if (this.message) { @@ -4497,10 +4793,10 @@ export class PokemonHealPhase extends CommonAnimPhase { } if (this.healStatus && lastStatusEffect && !hasMessage) { - this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectHealText(lastStatusEffect))); + this.scene.queueMessage(getStatusEffectHealText(lastStatusEffect, getPokemonNameWithAffix(pokemon))); } - if (fullHp && !lastStatusEffect) { + if (!healOrDamage && !lastStatusEffect) { super.end(); } } @@ -4630,7 +4926,9 @@ export class AttemptCapturePhase extends PokemonPhase { }); } }, - onComplete: () => this.catch() + onComplete: () => { + this.catch(); + } }); }; @@ -4671,7 +4969,6 @@ export class AttemptCapturePhase extends PokemonPhase { catch() { const pokemon = this.getPokemon() as EnemyPokemon; - this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); @@ -4695,8 +4992,9 @@ export class AttemptCapturePhase extends PokemonPhase { this.scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); - this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { + this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { const end = () => { + this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); this.scene.pokemonInfoContainer.hide(); this.removePb(); this.end(); @@ -4725,12 +5023,19 @@ export class AttemptCapturePhase extends PokemonPhase { } }); }; - Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { + Promise.all([pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon)]).then(() => { if (this.scene.getParty().length === 6) { const promptRelease = () => { - this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { - this.scene.pokemonInfoContainer.makeRoomForConfirmUi(); + this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { + this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); this.scene.ui.setMode(Mode.CONFIRM, () => { + const newPokemon = this.scene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon); + this.scene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => { + this.scene.ui.setMode(Mode.MESSAGE).then(() => { + promptRelease(); + }); + }, false); + }, () => { this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (slotIndex < 6) { @@ -4745,7 +5050,7 @@ export class AttemptCapturePhase extends PokemonPhase { removePokemon(); end(); }); - }); + }, "fullParty"); }); }; promptRelease(); @@ -4789,7 +5094,7 @@ export class AttemptRunPhase extends PokemonPhase { this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyField ].flat(), + targets: [this.scene.arenaEnemy, enemyField].flat(), alpha: 0, duration: 250, ease: "Sine.easeIn", @@ -4830,6 +5135,8 @@ export class SelectModifierPhase extends BattlePhase { if (!this.rerollCount) { this.updateSeed(); + } else { + this.scene.reroll = false; } const party = this.scene.getParty(); @@ -4855,33 +5162,41 @@ export class SelectModifierPhase extends BattlePhase { let cost: integer; switch (rowCursor) { case 0: - if (!cursor) { + 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"); } - } else if (cursor === 1) { - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, toSlotIndex: integer) => { + 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) { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)).then(() => { - 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, true); - }); + const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.isTransferrable && m.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); - } else { + 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)); @@ -4913,6 +5228,7 @@ export class SelectModifierPhase extends BattlePhase { 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 { @@ -4993,7 +5309,7 @@ export class SelectModifierPhase extends BattlePhase { getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): integer { let baseValue = 0; if (lockRarities) { - const tierValues = [ 50, 125, 300, 750, 2000 ]; + const tierValues = [50, 125, 300, 750, 2000]; for (const opt of typeOptions) { baseValue += tierValues[opt.type.tier]; } @@ -5025,14 +5341,19 @@ export class EggLapsePhase extends Phase { super.start(); const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => { - return Overrides.IMMEDIATE_HATCH_EGGS_OVERRIDE ? true : --egg.hatchWaves < 1; + return Overrides.EGG_IMMEDIATE_HATCH_OVERRIDE ? true : --egg.hatchWaves < 1; }); - if (eggsToHatch.length) { + let eggCount: integer = eggsToHatch.length; + + if (eggCount) { this.scene.queueMessage(i18next.t("battle:eggHatching")); for (const egg of eggsToHatch) { - this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg)); + this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg, eggCount)); + if (eggCount > 0) { + eggCount--; + } } } @@ -5172,7 +5493,7 @@ export class ScanIvsPhase extends PokemonPhase { const pokemon = this.getPokemon(); - this.scene.ui.showText(i18next.t("battle:ivScannerUseQuestion", { pokemonName: pokemon.name }), null, () => { + this.scene.ui.showText(i18next.t("battle:ivScannerUseQuestion", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.clearText(); @@ -5208,7 +5529,7 @@ export class TrainerMessageTestPhase extends BattlePhase { continue; } const config = trainerConfigs[type]; - [ config.encounterMessages, config.femaleEncounterMessages, config.victoryMessages, config.femaleVictoryMessages, config.defeatMessages, config.femaleDefeatMessages ] + [config.encounterMessages, config.femaleEncounterMessages, config.victoryMessages, config.femaleVictoryMessages, config.defeatMessages, config.femaleDefeatMessages] .map(messages => { if (messages?.length) { testMessages.push(...messages); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 543fc5bffa6..08d4bd9c162 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1,19 +1,19 @@ +import i18next from "i18next"; import BattleScene, { PokeballCounts, bypassLogin } from "../battle-scene"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "../field/pokemon"; import { pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions"; import PokemonSpecies, { allSpecies, getPokemonSpecies, noStarterFormKeys, speciesStarters } from "../data/pokemon-species"; -import { Species, defaultStarterSpecies } from "../data/enums/species"; import * as Utils from "../utils"; -import * as Overrides from "../overrides"; +import Overrides from "#app/overrides"; import PokemonData from "./pokemon-data"; import PersistentModifierData from "./modifier-data"; import ArenaData from "./arena-data"; import { Unlockables } from "./unlockables"; -import { GameModes, gameModes } from "../game-mode"; +import { GameModes, getGameMode } from "../game-mode"; import { BattleType } from "../battle"; import TrainerData from "./trainer-data"; import { trainerConfigs } from "../data/trainer-config"; -import { Setting, setSetting, settingDefaults } from "./settings"; +import { SettingKeys, resetSettings, setSetting } from "./settings/settings"; import { achvs } from "./achv"; import EggData from "./egg-data"; import { Egg } from "../data/egg"; @@ -24,34 +24,39 @@ import { clientSessionId, loggedInUser, updateUserInfo } from "../account"; import { Nature } from "../data/nature"; import { GameStats } from "./game-stats"; import { Tutorial } from "../tutorial"; -import { Moves } from "../data/enums/moves"; import { speciesEggMoves } from "../data/egg-moves"; import { allMoves } from "../data/move"; import { TrainerVariant } from "../field/trainer"; import { OutdatedPhase, ReloadSessionPhase } from "#app/phases"; import { Variant, variantData } from "#app/data/variant"; +import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings/settings-gamepad"; +import {setSettingKeyboard, SettingKeyboard} from "#app/system/settings/settings-keyboard"; +import { TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena.js"; +import { EnemyAttackStatusEffectChanceModifier } from "../modifier/modifier"; +import { StatusEffect } from "#app/data/status-effect.js"; +import ChallengeData from "./challenge-data"; +import { Device } from "#enums/devices"; +import { GameDataType } from "#enums/game-data-type"; +import { Moves } from "#enums/moves"; +import { PlayerGender } from "#enums/player-gender"; +import { Species } from "#enums/species"; +import { applyChallenges, ChallengeType } from "#app/data/challenge.js"; +import { Abilities } from "#app/enums/abilities.js"; + +export const defaultStarterSpecies: Species[] = [ + Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, + Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, + Species.TREECKO, Species.TORCHIC, Species.MUDKIP, + Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, + Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, + Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, + Species.ROWLET, Species.LITTEN, Species.POPPLIO, + Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, + Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY +]; const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary -export enum GameDataType { - SYSTEM, - SESSION, - SETTINGS, - TUTORIALS, - RUN_HISTORY -} - -export enum PlayerGender { - UNSET, - MALE, - FEMALE -} - -export enum Passive { - UNLOCKED = 1, - ENABLED = 2 -} - export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): string { switch (dataType) { case GameDataType.SYSTEM: @@ -66,18 +71,20 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str return "settings"; case GameDataType.TUTORIALS: return "tutorials"; + case GameDataType.SEEN_DIALOGUES: + return "seenDialogues"; case GameDataType.RUN_HISTORY: return "runHistory"; } } -function encrypt(data: string, bypassLogin: boolean): string { +export function encrypt(data: string, bypassLogin: boolean): string { return (bypassLogin ? (data: string) => btoa(data) : (data: string) => AES.encrypt(data, saveKey))(data); } -function decrypt(data: string, bypassLogin: boolean): string { +export function decrypt(data: string, bypassLogin: boolean): string { return (bypassLogin ? (data: string) => atob(data) : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data); @@ -90,7 +97,6 @@ interface SystemSaveData { dexData: DexData; starterData: StarterData; gameStats: GameStats; - runHistory: RunHistoryData; unlocks: Unlocks; achvUnlocks: AchvUnlocks; voucherUnlocks: VoucherUnlocks; @@ -98,6 +104,8 @@ interface SystemSaveData { eggs: EggData[]; gameVersion: string; timestamp: integer; + eggPity: integer[]; + unlockPity: integer[]; } export interface SessionSaveData { @@ -117,15 +125,7 @@ export interface SessionSaveData { trainer: TrainerData; gameVersion: string; timestamp: integer; -} - -export interface RunHistoryData { - [key: integer]: RunEntries; -} - -export interface RunEntries { - entry: SessionSaveData; - victory: boolean; + challenges: ChallengeData[]; } interface Unlocks { @@ -141,7 +141,7 @@ interface VoucherUnlocks { } export interface VoucherCounts { - [type: string]: integer; + [type: string]: integer; } export interface DexData { @@ -182,6 +182,15 @@ export const AbilityAttr = { ABILITY_HIDDEN: 4 }; +export interface RunHistoryData { + [key: integer]: RunEntries; +} + +export interface RunEntries { + entry: SessionSaveData; + victory: boolean; +} + export type StarterMoveset = [ Moves ] | [ Moves, Moves ] | [ Moves, Moves, Moves ] | [ Moves, Moves, Moves, Moves ]; export interface StarterFormMoveData { @@ -192,6 +201,46 @@ export interface StarterMoveData { [key: integer]: StarterMoveset | StarterFormMoveData } +export interface StarterAttributes { + nature?: integer; + ability?: integer; + variant?: integer; + form?: integer; + female?: boolean; +} + +export interface StarterPreferences { + [key: integer]: StarterAttributes; +} + +// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. +// if they ever add private static variables, move this into StarterPrefs +const StarterPrefers_DEFAULT : string = "{}"; +let StarterPrefers_private_latest : string = StarterPrefers_DEFAULT; + +// This is its own class as StarterPreferences... +// - don't need to be loaded on startup +// - isn't stored with other data +// - don't require to be encrypted +// - shouldn't require calls outside of the starter selection +export class StarterPrefs { + // called on starter selection show once + static load(): StarterPreferences { + return JSON.parse( + StarterPrefers_private_latest = (localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT) + ); + } + + // called on starter selection clear, always + static save(prefs: StarterPreferences): void { + const pStr : string = JSON.stringify(prefs); + if (pStr !== StarterPrefers_private_latest) { + // something changed, store the update + localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr); + } + } +} + export interface StarterDataEntry { moveset: StarterMoveset | StarterFormMoveData; eggMoves: integer; @@ -211,6 +260,10 @@ export interface TutorialFlags { [key: string]: boolean } +export interface SeenDialogues { + [key: string]: boolean; +} + const systemShortKeys = { seenAttr: "$sa", caughtAttr: "$ca", @@ -243,7 +296,7 @@ export class GameData { public starterData: StarterData; public gameStats: GameStats; - public runHistory: RunHistoryData; + public unlocks: Unlocks; public achvUnlocks: AchvUnlocks; @@ -251,14 +304,17 @@ export class GameData { public voucherUnlocks: VoucherUnlocks; public voucherCounts: VoucherCounts; public eggs: Egg[]; + public eggPity: integer[]; + public unlockPity: integer[]; constructor(scene: BattleScene) { this.scene = scene; this.loadSettings(); + this.loadGamepadSettings(); + this.loadMappingConfigs(); this.trainerId = Utils.randInt(65536); this.secretId = Utils.randInt(65536); this.starterData = {}; - this.runHistory = {}; this.gameStats = new GameStats(); this.unlocks = { [Unlockables.ENDLESS_MODE]: false, @@ -267,6 +323,7 @@ export class GameData { }; this.achvUnlocks = {}; this.voucherUnlocks = {}; + this.runHistory = {}; this.voucherCounts = { [VoucherType.REGULAR]: 0, [VoucherType.PLUS]: 0, @@ -274,6 +331,8 @@ export class GameData { [VoucherType.GOLDEN]: 0 }; this.eggs = []; + this.eggPity = [0, 0, 0, 0]; + this.unlockPity = [0, 0, 0, 0]; this.initDexData(); this.initStarterData(); } @@ -283,7 +342,6 @@ export class GameData { trainerId: this.trainerId, secretId: this.secretId, gender: this.gender, - runHistory: this.runHistory, dexData: this.dexData, starterData: this.starterData, gameStats: this.gameStats, @@ -293,7 +351,9 @@ export class GameData { voucherCounts: this.voucherCounts, eggs: this.eggs.map(e => new EggData(e)), gameVersion: this.scene.game.config.gameVersion, - timestamp: new Date().getTime() + timestamp: new Date().getTime(), + eggPity: this.eggPity.slice(0), + unlockPity: this.unlockPity.slice(0) }; } @@ -308,7 +368,7 @@ export class GameData { localStorage.setItem(`data_${loggedInUser.username}`, encrypt(systemData, bypassLogin)); if (!bypassLogin) { - Utils.apiPost(`savedata/update?datatype=${GameDataType.SYSTEM}&clientSessionId=${clientSessionId}`, systemData, undefined, true) + Utils.apiPost(`savedata/system/update?clientSessionId=${clientSessionId}`, systemData, undefined, true) .then(response => response.text()) .then(error => { this.scene.ui.savingIcon.hide(); @@ -342,7 +402,7 @@ export class GameData { } if (!bypassLogin) { - Utils.apiFetch(`savedata/system?clientSessionId=${clientSessionId}`, true) + Utils.apiFetch(`savedata/system/get?clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== "{") { @@ -386,23 +446,22 @@ export class GameData { localStorage.setItem(`data_${loggedInUser.username}`, encrypt(systemDataStr, bypassLogin)); - if (!localStorage.hasOwnProperty(`runHistoryData_${loggedInUser.username}`)) { - localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt("", true)); - } - - /*const versions = [ this.scene.game.config.gameVersion, data.gameVersion || '0.0.0' ]; if (versions[0] !== versions[1]) { const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); }*/ + if (!localStorage.hasOwnProperty(`runHistoryData_${loggedInUser.username}`)) { + localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt("", true)); + } + this.trainerId = systemData.trainerId; this.secretId = systemData.secretId; this.gender = systemData.gender; - this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); const initStarterData = !systemData.starterData; @@ -481,6 +540,9 @@ export class GameData { ? systemData.eggs.map(e => e.toEgg()) : []; + this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0]; + this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0]; + this.dexData = Object.assign(this.dexData, systemData.dexData); this.consolidateDexData(this.dexData); this.defaultDexData = null; @@ -504,7 +566,67 @@ export class GameData { }); } - private parseSystemData(dataStr: string): SystemSaveData { + public async getRunHistoryData(scene: BattleScene): Promise { + if (!Utils.isLocal) { + const data = await Utils.apiFetch("savedata/runHistory", true).json(); + //const data = await response.json(); + if (localStorage.hasOwnProperty(`runHistoryData_${loggedInUser.username}`)) { + let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + if (cachedResponse) { + cachedResponse = JSON.parse(decrypt(cachedResponse, true)); + } + const cachedRHData = cachedResponse ?? {}; + //check to see whether cachedData or serverData is more up-to-date + if ( Object.keys(cachedRHData).length >= Object.keys(data).length ) { + return cachedRHData; + } + } else { + localStorage.setItem(`runHistoryData_${loggedInUser.username}`, JSON.parse(encrypt({}, true))); + return {}; + } + return data; + } else { + let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + if (cachedResponse) { + cachedResponse = JSON.parse(decrypt(cachedResponse, true)); + } + const cachedRHData = cachedResponse ?? {}; + return cachedRHData; + } + } + + async saveRunHistory(scene: BattleScene, runEntry : SessionSaveData, victory: boolean): Promise { + + let runHistoryData = await this.getRunHistoryData(scene); + if (!runHistoryData) { + runHistoryData = {}; + } + const timestamps = Object.keys(runHistoryData); + + //Arbitrary limit of 25 entries per User --> Can increase or decrease + if (timestamps.length >= 25) { + delete this.scene.gameData.runHistory[Math.min(timestamps)]; + } + + const timestamp = (runEntry.timestamp).toString(); + runHistoryData[timestamp] = {}; + runHistoryData[timestamp]["entry"] = runEntry; + runHistoryData[timestamp]["victory"] = victory; + + localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt(JSON.stringify(runHistoryData), true)); + + if (!Utils.isLocal) { + try { + Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); + return true; + } catch (err) { + console.log("savedata/runHistory POST failed : ", err); + return false; + } + } + } + + parseSystemData(dataStr: string): SystemSaveData { return JSON.parse(dataStr, (k: string, v: any) => { if (k === "gameStats") { return new GameStats(v); @@ -523,11 +645,13 @@ export class GameData { }) as SystemSaveData; } - private convertSystemDataStr(dataStr: string, shorten: boolean = false): string { + convertSystemDataStr(dataStr: string, shorten: boolean = false): string { if (!shorten) { // Account for past key oversight dataStr = dataStr.replace(/\$pAttr/g, "$pa"); } + dataStr = dataStr.replace(/"trainerId":\d+/g, `"trainerId":${this.trainerId}`); + dataStr = dataStr.replace(/"secretId":\d+/g, `"secretId":${this.secretId}`); const fromKeys = shorten ? Object.keys(systemShortKeys) : Object.values(systemShortKeys); const toKeys = shorten ? Object.values(systemShortKeys) : Object.keys(systemShortKeys); for (const k in fromKeys) { @@ -542,7 +666,7 @@ export class GameData { return true; } - const response = await Utils.apiPost("savedata/system/verify", JSON.stringify({ clientSessionId: clientSessionId }), undefined, true) + const response = await Utils.apiFetch(`savedata/system/verify?clientSessionId=${clientSessionId}`, true) .then(response => response.json()); if (!response.valid) { @@ -565,27 +689,123 @@ export class GameData { } } - public saveSetting(setting: Setting, valueIndex: integer): boolean { + /** + * Saves a setting to localStorage + * @param setting string ideally of SettingKeys + * @param valueIndex index of the setting's option + * @returns true + */ + public saveSetting(setting: string, valueIndex: integer): boolean { let settings: object = {}; if (localStorage.hasOwnProperty("settings")) { settings = JSON.parse(localStorage.getItem("settings")); } - setSetting(this.scene, setting as Setting, valueIndex); + setSetting(this.scene, setting, valueIndex); - Object.keys(settingDefaults).forEach(s => { - if (s === setting) { - settings[s] = valueIndex; - } - }); + settings[setting] = valueIndex; localStorage.setItem("settings", JSON.stringify(settings)); return true; } + /** + * Saves the mapping configurations for a specified device. + * + * @param deviceName - The name of the device for which the configurations are being saved. + * @param config - The configuration object containing custom mapping details. + * @returns `true` if the configurations are successfully saved. + */ + public saveMappingConfigs(deviceName: string, config): boolean { + const key = deviceName.toLowerCase(); // Convert the gamepad name to lowercase to use as a key + let mappingConfigs: object = {}; // Initialize an empty object to hold the mapping configurations + if (localStorage.hasOwnProperty("mappingConfigs")) {// Check if 'mappingConfigs' exists in localStorage + mappingConfigs = JSON.parse(localStorage.getItem("mappingConfigs")); + } // Parse the existing 'mappingConfigs' from localStorage + if (!mappingConfigs[key]) { + mappingConfigs[key] = {}; + } // If there is no configuration for the given key, create an empty object for it + mappingConfigs[key].custom = config.custom; // Assign the custom configuration to the mapping configuration for the given key + localStorage.setItem("mappingConfigs", JSON.stringify(mappingConfigs)); // Save the updated mapping configurations back to localStorage + return true; // Return true to indicate the operation was successful + } + + /** + * Loads the mapping configurations from localStorage and injects them into the input controller. + * + * @returns `true` if the configurations are successfully loaded and injected; `false` if no configurations are found in localStorage. + * + * @remarks + * This method checks if the 'mappingConfigs' entry exists in localStorage. If it does not exist, the method returns `false`. + * If 'mappingConfigs' exists, it parses the configurations and injects each configuration into the input controller + * for the corresponding gamepad or device key. The method then returns `true` to indicate success. + */ + public loadMappingConfigs(): boolean { + if (!localStorage.hasOwnProperty("mappingConfigs")) {// Check if 'mappingConfigs' exists in localStorage + return false; + } // If 'mappingConfigs' does not exist, return false + + const mappingConfigs = JSON.parse(localStorage.getItem("mappingConfigs")); // Parse the existing 'mappingConfigs' from localStorage + + for (const key of Object.keys(mappingConfigs)) {// Iterate over the keys of the mapping configurations + this.scene.inputController.injectConfig(key, mappingConfigs[key]); + } // Inject each configuration into the input controller for the corresponding key + + return true; // Return true to indicate the operation was successful + } + + public resetMappingToFactory(): boolean { + if (!localStorage.hasOwnProperty("mappingConfigs")) {// Check if 'mappingConfigs' exists in localStorage + return false; + } // If 'mappingConfigs' does not exist, return false + localStorage.removeItem("mappingConfigs"); + this.scene.inputController.resetConfigs(); + } + + /** + * Saves a gamepad setting to localStorage. + * + * @param setting - The gamepad setting to save. + * @param valueIndex - The index of the value to set for the gamepad setting. + * @returns `true` if the setting is successfully saved. + * + * @remarks + * This method initializes an empty object for gamepad settings if none exist in localStorage. + * It then updates the setting in the current scene and iterates over the default gamepad settings + * to update the specified setting with the new value. Finally, it saves the updated settings back + * to localStorage and returns `true` to indicate success. + */ + public saveControlSetting(device: Device, localStoragePropertyName: string, setting: SettingGamepad|SettingKeyboard, settingDefaults, valueIndex: integer): boolean { + let settingsControls: object = {}; // Initialize an empty object to hold the gamepad settings + + if (localStorage.hasOwnProperty(localStoragePropertyName)) { // Check if 'settingsControls' exists in localStorage + settingsControls = JSON.parse(localStorage.getItem(localStoragePropertyName)); // Parse the existing 'settingsControls' from localStorage + } + + if (device === Device.GAMEPAD) { + setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex); // Set the gamepad setting in the current scene + } else if (device === Device.KEYBOARD) { + setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); // Set the keyboard setting in the current scene + } + + Object.keys(settingDefaults).forEach(s => { // Iterate over the default gamepad settings + if (s === setting) {// If the current setting matches, update its value + settingsControls[s] = valueIndex; + } + }); + + localStorage.setItem(localStoragePropertyName, JSON.stringify(settingsControls)); // Save the updated gamepad settings back to localStorage + + return true; // Return true to indicate the operation was successful + } + + /** + * Loads Settings from local storage if available + * @returns true if succesful, false if not + */ private loadSettings(): boolean { - Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); + resetSettings(this.scene); if (!localStorage.hasOwnProperty("settings")) { return false; @@ -594,14 +814,28 @@ export class GameData { const settings = JSON.parse(localStorage.getItem("settings")); for (const setting of Object.keys(settings)) { - setSetting(this.scene, setting as Setting, settings[setting]); + setSetting(this.scene, setting, settings[setting]); + } + } + + private loadGamepadSettings(): boolean { + Object.values(SettingGamepad).map(setting => setting as SettingGamepad).forEach(setting => setSettingGamepad(this.scene, setting, settingGamepadDefaults[setting])); + + if (!localStorage.hasOwnProperty("settingsGamepad")) { + return false; + } + const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")); + + for (const setting of Object.keys(settingsGamepad)) { + setSettingGamepad(this.scene, setting as SettingGamepad, settingsGamepad[setting]); } } public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { + const key = getDataTypeKey(GameDataType.TUTORIALS); let tutorials: object = {}; - if (localStorage.hasOwnProperty("tutorials")) { - tutorials = JSON.parse(localStorage.getItem("tutorials")); + if (localStorage.hasOwnProperty(key)) { + tutorials = JSON.parse(localStorage.getItem(key)); } Object.keys(Tutorial).map(t => t as Tutorial).forEach(t => { @@ -613,20 +847,21 @@ export class GameData { } }); - localStorage.setItem("tutorials", JSON.stringify(tutorials)); + localStorage.setItem(key, JSON.stringify(tutorials)); return true; } public getTutorialFlags(): TutorialFlags { + const key = getDataTypeKey(GameDataType.TUTORIALS); const ret: TutorialFlags = {}; Object.values(Tutorial).map(tutorial => tutorial as Tutorial).forEach(tutorial => ret[Tutorial[tutorial]] = false); - if (!localStorage.hasOwnProperty("tutorials")) { + if (!localStorage.hasOwnProperty(key)) { return ret; } - const tutorials = JSON.parse(localStorage.getItem("tutorials")); + const tutorials = JSON.parse(localStorage.getItem(key)); for (const tutorial of Object.keys(tutorials)) { ret[tutorial] = tutorials[tutorial]; @@ -635,6 +870,34 @@ export class GameData { return ret; } + public saveSeenDialogue(dialogue: string): boolean { + const key = getDataTypeKey(GameDataType.SEEN_DIALOGUES); + const dialogues: object = this.getSeenDialogues(); + + dialogues[dialogue] = true; + localStorage.setItem(key, JSON.stringify(dialogues)); + console.log("Dialogue saved as seen:", dialogue); + + return true; + } + + public getSeenDialogues(): SeenDialogues { + const key = getDataTypeKey(GameDataType.SEEN_DIALOGUES); + const ret: SeenDialogues = {}; + + if (!localStorage.hasOwnProperty(key)) { + return ret; + } + + const dialogues = JSON.parse(localStorage.getItem(key)); + + for (const dialogue of Object.keys(dialogues)) { + ret[dialogue] = dialogues[dialogue]; + } + + return ret; + } + private getSessionSaveData(scene: BattleScene): SessionSaveData { return { seed: scene.seed, @@ -652,7 +915,8 @@ export class GameData { battleType: scene.currentBattle.battleType, trainer: scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null, gameVersion: scene.game.config.gameVersion, - timestamp: new Date().getTime() + timestamp: new Date().getTime(), + challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)) } as SessionSaveData; } @@ -664,6 +928,14 @@ export class GameData { const handleSessionData = async (sessionDataStr: string) => { try { const sessionData = this.parseSessionData(sessionDataStr); + for (let i = 0; i <= 5; i++) { + const speciesToCheck = getPokemonSpecies(sessionData.party[i]?.species); + if (sessionData.party[i]?.abilityIndex === 1) { + if (speciesToCheck.ability1 === speciesToCheck.ability2 && speciesToCheck.abilityHidden !== Abilities.NONE && speciesToCheck.abilityHidden !== speciesToCheck.ability1) { + sessionData.party[i].abilityIndex = 2; + } + } + } resolve(sessionData); } catch (err) { reject(err); @@ -672,7 +944,7 @@ export class GameData { }; if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser.username}`)) { - Utils.apiFetch(`savedata/session?slot=${slotId}&clientSessionId=${clientSessionId}`, true) + Utils.apiFetch(`savedata/session/get?slot=${slotId}&clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(async response => { if (!response.length || response[0] !== "{") { @@ -698,10 +970,13 @@ export class GameData { loadSession(scene: BattleScene, slotId: integer, sessionData?: SessionSaveData): Promise { return new Promise(async (resolve, reject) => { try { - const initSessionFromData = async sessionData => { + const initSessionFromData = async (sessionData: SessionSaveData) => { console.debug(sessionData); - scene.gameMode = gameModes[sessionData.gameMode || GameModes.CLASSIC]; + scene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); + if (sessionData.challenges) { + scene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); + } scene.setSeed(sessionData.seed || scene.game.config.seed[0]); scene.resetSeed(); @@ -760,6 +1035,10 @@ export class GameData { }); scene.arena.weather = sessionData.arena.weather; + scene.arena.eventTarget.dispatchEvent(new WeatherChangedEvent(null, scene.arena.weather?.weatherType, scene.arena.weather?.turnsLeft)); + + scene.arena.terrain = sessionData.arena.terrain; + scene.arena.eventTarget.dispatchEvent(new TerrainChangedEvent(null, scene.arena.terrain?.terrainType, scene.arena.terrain?.turnsLeft)); // TODO //scene.arena.tags = sessionData.arena.tags; @@ -813,7 +1092,7 @@ export class GameData { if (success !== null && !success) { return resolve(false); } - Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { + Utils.apiFetch(`savedata/session/delete?slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { if (response.ok) { loggedInUser.lastSessionSlot = -1; localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ""}_${loggedInUser.username}`); @@ -877,7 +1156,7 @@ export class GameData { return resolve([false, false]); } const sessionData = this.getSessionSaveData(scene); - Utils.apiPost(`savedata/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true).then(response => { + Utils.apiPost(`savedata/session/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true).then(response => { if (response.ok) { loggedInUser.lastSessionSlot = -1; localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ""}_${loggedInUser.username}`); @@ -931,6 +1210,9 @@ export class GameData { if (md?.className === "ExpBalanceModifier") { // Temporarily limit EXP Balance until it gets reworked md.stackCount = Math.min(md.stackCount, 4); } + if (md instanceof EnemyAttackStatusEffectChanceModifier && md.effect === StatusEffect.FREEZE || md.effect === StatusEffect.SLEEP) { + continue; + } ret.push(new PersistentModifierData(md, player)); } return ret; @@ -940,70 +1222,21 @@ export class GameData { return new ArenaData(v); } + if (k === "challenges") { + const ret: ChallengeData[] = []; + if (v === null) { + v = []; + } + for (const c of v) { + ret.push(new ChallengeData(c)); + } + return ret; + } + return v; }) as SessionSaveData; } - public async getRunHistoryData(scene: BattleScene): Promise { - if (!Utils.isLocal) { - const data = await Utils.apiFetch("savedata/runHistory", true).json(); - //const data = await response.json(); - if (localStorage.hasOwnProperty(`runHistoryData_${loggedInUser.username}`)) { - let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); - if (cachedResponse) { - cachedResponse = JSON.parse(decrypt(cachedResponse, true)); - } - const cachedRHData = cachedResponse ?? {}; - //check to see whether cachedData or serverData is more up-to-date - if ( Object.keys(cachedRHData).length >= Object.keys(data).length ) { - return cachedRHData; - } - } else { - localStorage.setItem(`runHistoryData_${loggedInUser.username}`, JSON.parse(encrypt({}, true))); - return {}; - } - return data; - } else { - let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); - if (cachedResponse) { - cachedResponse = JSON.parse(decrypt(cachedResponse, true)); - } - const cachedRHData = cachedResponse ?? {}; - return cachedRHData; - } - } - - async saveRunHistory(scene: BattleScene, runEntry : SessionSaveData, victory: boolean): Promise { - - let runHistoryData = await this.getRunHistoryData(scene); - if (!runHistoryData) { - runHistoryData = {}; - } - const timestamps = Object.keys(runHistoryData); - - //Arbitrary limit of 25 entries per User --> Can increase or decrease - if (timestamps.length >= 25) { - delete this.scene.gameData.runHistory[Math.min(timestamps)]; - } - - const timestamp = (runEntry.timestamp).toString(); - runHistoryData[timestamp] = {}; - runHistoryData[timestamp]["entry"] = runEntry; - runHistoryData[timestamp]["victory"] = victory; - - localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt(JSON.stringify(runHistoryData), true)); - - if (!Utils.isLocal) { - try { - Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); - return true; - } catch (err) { - console.log("savedata/runHistory POST failed : ", err); - return false; - } - } - } - saveAll(scene: BattleScene, skipVerification: boolean = false, sync: boolean = false, useCachedSession: boolean = false, useCachedSystem: boolean = false): Promise { return new Promise(resolve => { Utils.executeIf(!skipVerification, updateUserInfo).then(success => { @@ -1080,7 +1313,7 @@ export class GameData { link.remove(); }; if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) + Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}/get?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== "{") { @@ -1121,9 +1354,11 @@ export class GameData { reader.onload = (_ => { return e => { + let dataName: string; let dataStr = AES.decrypt(e.target.result.toString(), saveKey).toString(enc.Utf8); let valid = false; try { + dataName = GameDataType[dataType].toLowerCase(); switch (dataType) { case GameDataType.SYSTEM: dataStr = this.convertSystemDataStr(dataStr); @@ -1143,38 +1378,28 @@ export class GameData { console.error(ex); } - let dataName: string; - switch (dataType) { - case GameDataType.SYSTEM: - dataName = "save"; - break; - case GameDataType.SESSION: - dataName = "session"; - break; - case GameDataType.SETTINGS: - dataName = "settings"; - break; - case GameDataType.TUTORIALS: - dataName = "tutorials"; - break; - } - const displayError = (error: string) => this.scene.ui.showText(error, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500)); if (!valid) { return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500)); } - this.scene.ui.revertMode(); + this.scene.ui.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => { this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { localStorage.setItem(dataKey, encrypt(dataStr, bypassLogin)); if (!bypassLogin && dataType < GameDataType.SETTINGS) { updateUserInfo().then(success => { - if (!success) { + if (!success[0]) { return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`); } - Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, dataStr, undefined, true) + let url: string; + if (dataType === GameDataType.SESSION) { + url = `savedata/session/update?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`; + } else { + url = `savedata/system/update?trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`; + } + Utils.apiPost(url, dataStr, undefined, true) .then(response => response.text()) .then(error => { if (error) { @@ -1349,7 +1574,7 @@ export class GameData { if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) { this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(`${species.name} has been\nadded as a starter!`, null, () => checkPrevolution(), null, true); + this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(), null, true); } else { checkPrevolution(); } @@ -1415,7 +1640,9 @@ export class GameData { this.starterData[speciesId].eggMoves |= value; this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(`${eggMoveIndex === 3 ? "Rare " : ""}Egg Move unlocked: ${allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name}`, null, () => resolve(true), null, true); + + const moveName = allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name; + this.scene.ui.showText(eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }), null, () => resolve(true), null, true); }); } @@ -1537,7 +1764,10 @@ export class GameData { value = decrementValue(value); } - return value; + const cost = new Utils.NumberHolder(value); + applyChallenges(this.scene.gameMode, ChallengeType.STARTER_COST, speciesId, cost); + + return cost.value; } getFormIndex(attr: bigint): integer {