From 4fd321790ad5c624b1dcb62edf688ad9f1197b00 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 07:45:17 -0700 Subject: [PATCH 01/35] Fixed SaveSessionData issue + Added loss details + removed Modifiers --- src/system/game-data.ts | 2 +- src/ui/run-history-ui-handler.ts | 326 +++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 src/ui/run-history-ui-handler.ts diff --git a/src/system/game-data.ts b/src/system/game-data.ts index bbc74ee13e3..aa04bea5170 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1101,7 +1101,7 @@ export class GameData { }); } - parseSessionData(dataStr: string): SessionSaveData { + public parseSessionData(dataStr: string): SessionSaveData { return JSON.parse(dataStr, (k: string, v: any) => { /*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ]; diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts new file mode 100644 index 00000000000..edca87b9e74 --- /dev/null +++ b/src/ui/run-history-ui-handler.ts @@ -0,0 +1,326 @@ +import BattleScene from "../battle-scene"; +import { gameModes } from "../game-mode"; +import { SessionSaveData, parseSessionData, RunHistoryData, RunEntries } from "../system/game-data"; +import { TextStyle, addTextObject } from "./text"; +import { Mode } from "./ui"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import { PokemonData } from "../system/pokemon-data"; +import { TrainerData } from "../system/trainer-data" +import Pokemon, { EnemyPokemon, PlayerPokemon } from "../field/pokemon"; +import { PokemonHeldItemModifier } from "../modifier/modifier"; +import MessageUiHandler from "./message-ui-handler"; +import i18next from "i18next"; +import {Button} from "../enums/buttons"; +import { BattleType } from "../battle"; +import {TrainerType} from "../data/enums/trainer-type"; + +export const runCount = 25; + +export type RunSelectCallback = (cursor: integer) => void; + +export default class RunHistoryUiHandler extends MessageUiHandler { + + private runSelectContainer: Phaser.GameObjects.Container; + private runsContainer: Phaser.GameObjects.Container; + private runSelectMessageBox: Phaser.GameObjects.NineSlice; + private runSelectMessageBoxContainer: Phaser.GameObjects.Container; + private runs: runEntry[]; + + private uiMode: RunHistoryUiMode; + private runSelectCallback: RunSelectCallback; + + private scrollCursor: integer = 0; + + private cursorObj: Phaser.GameObjects.NineSlice; + + private runContainerInitialY: number; + + constructor(scene: BattleScene) { + super(scene, Mode.RUN_HISTORY); + } + + setup() { + const ui = this.getUi(); + + this.runSelectContainer = this.scene.add.container(0, 0); + this.runSelectContainer.setVisible(false); + ui.add(this.runSelectContainer); + + const loadSessionBg = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, -this.scene.game.canvas.height / 6, 0x006860); + loadSessionBg.setOrigin(0, 0); + this.runSelectContainer.add(loadSessionBg); + + this.runContainerInitialY = -this.scene.game.canvas.height / 6 + 8; + + this.runsContainer = this.scene.add.container(8, this.runContainerInitialY); + this.runSelectContainer.add(this.runsContainer); + + this.runSelectMessageBoxContainer = this.scene.add.container(0, 0); + this.runSelectMessageBoxContainer.setVisible(false); + this.runSelectContainer.add(this.runSelectMessageBoxContainer); + + this.runSelectMessageBox = addWindow(this.scene, 1, -1, 318, 28); + this.runSelectMessageBox.setOrigin(0, 1); + this.runSelectMessageBoxContainer.add(this.runSelectMessageBox); + + this.message = addTextObject(this.scene, 8, 8, "", TextStyle.WINDOW, { maxLines: 2 }); + this.message.setOrigin(0, 0); + this.runSelectMessageBoxContainer.add(this.message); + + this.runs = []; + } + + show(args: any[]): boolean { + super.show(args); + + this.getUi().bringToTop(this.runSelectContainer); + this.runSelectContainer.setVisible(true); + this.populateruns(); + + this.setScrollCursor(0); + this.setCursor(0); + + return true; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + const error = false; + + if (button === Button.ACTION || button === Button.CANCEL) { + const originalCallback = this.runSelectCallback; + if (button === Button.ACTION) { + const cursor = this.cursor + this.scrollCursor; + console.log("Action --> page with more detailed run information"); + success = true; + return success; + } else { + this.runSelectCallback = null; + success = true; + this.scene.ui.revertMode(); + } + } else { + switch (button) { + case Button.UP: + if (this.cursor) { + success = this.setCursor(this.cursor - 1); + } else if (this.scrollCursor) { + success = this.setScrollCursor(this.scrollCursor - 1); + } + break; + case Button.DOWN: + if (this.cursor < 2) { + success = this.setCursor(this.cursor + 1); + } else if (this.scrollCursor < runCount - 3) { + success = this.setScrollCursor(this.scrollCursor + 1); + } + break; + } + } + + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + return success || error; + } + + + populateruns() { + const timestamps = Object.keys(this.scene.gameData.runHistory); + if (timestamps.length > 1) { + timestamps.sort((a, b) => a - b); + } + const entryCount = timestamps.length; + console.log(entryCount); + for (let s = 0; s < entryCount; s++) { + const entry = new RunEntry(this.scene, timestamps[s], s); + this.scene.add.existing(entry); + this.runsContainer.add(entry); + this.runs.push(entry); + } + } + + showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); + + if (text?.indexOf("\n") === -1) { + this.runSelectMessageBox.setSize(318, 28); + this.message.setY(-22); + } else { + this.runSelectMessageBox.setSize(318, 42); + this.message.setY(-37); + } + + this.runSelectMessageBoxContainer.setVisible(!!text?.length); + } + + setCursor(cursor: integer): boolean { + const changed = super.setCursor(cursor); + + if (!this.cursorObj) { + this.cursorObj = this.scene.add.nineslice(0, 0, "select_cursor_highlight_thick", null, 296, 44, 6, 6, 6, 6); + this.cursorObj.setOrigin(0, 0); + this.runsContainer.add(this.cursorObj); + } + this.cursorObj.setPosition(4, 4 + (cursor + this.scrollCursor) * 56); + + return changed; + } + + setScrollCursor(scrollCursor: integer): boolean { + const changed = scrollCursor !== this.scrollCursor; + + if (changed) { + this.scrollCursor = scrollCursor; + this.setCursor(this.cursor); + this.scene.tweens.add({ + targets: this.runsContainer, + y: this.runContainerInitialY - 56 * scrollCursor, + duration: Utils.fixedInt(325), + ease: "Sine.easeInOut" + }); + } + + return changed; + } + + clear() { + super.clear(); + this.runSelectContainer.setVisible(false); + this.eraseCursor(); + this.runSelectCallback = null; + this.clearruns(); + } + + eraseCursor() { + if (this.cursorObj) { + this.cursorObj.destroy(); + } + this.cursorObj = null; + } + + clearruns() { + this.runs.splice(0, this.runs.length); + this.runsContainer.removeAll(true); + } +} + +class RunEntry extends Phaser.GameObjects.Container { + public slotId: integer; + public hasData: boolean; + private loadingLabel: Phaser.GameObjects.Text; + + constructor(scene: BattleScene, timestamp: string, slotId: integer) { + super(scene, 0, slotId*56); + + this.slotId = slotId; + + this.setup(this.scene.gameData.runHistory[timestamp]); + + } + + setup(run: RunHistoryData) { + + const victory = run.victory; + const data = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); + console.log(data); + + const slotWindow = addWindow(this.scene, 0, 0, 304, 52); + this.add(slotWindow); + + + if (victory) { + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Victory", TextStyle.WINDOW); + this.add(gameOutcomeLabel); + } else { + if (data.battleType === BattleType.WILD) { + const enemyContainer = this.scene.add.container(8,5); + const gameOutcomeLabel = addTextObject(this.scene, 0, 0, "Defeated by ", TextStyle.WINDOW); + enemyContainer.add(gameOutcomeLabel); + const enemyIconContainer = this.scene.add.container(58,-8); + enemyIconContainer.setScale(0.75); + data.enemyParty.forEach((enemyData, e) => { + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemyLevel = addTextObject(this.scene, 32, 20, `Lv${Utils.formatLargeNumber(enemy.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); + enemyLevel.setShadow(0, 0, null); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(1, 0); + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyContainer.add(enemyIconContainer); + enemy.destroy(); + }); + this.add(enemyContainer); + } + else if (data.battleType === BattleType.TRAINER) { + const tObj = data.trainer.toTrainer(this.scene); + const tType = TrainerType[data.trainer.trainerType]; + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `Defeated by ${tType.charAt(0)+tType.substring(1).toLowerCase()}`, TextStyle.WINDOW); + this.add(gameOutcomeLabel); + } + } + + const gameModeLabel = addTextObject(this.scene, 8, 19, `${gameModes[data.gameMode]?.getName() || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); + this.add(gameModeLabel); + + const timestampLabel = addTextObject(this.scene, 8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); + this.add(timestampLabel); + + const pokemonIconsContainer = this.scene.add.container(144, 4); + + data.party.forEach((p: PokemonData, i: integer) => { + const iconContainer = this.scene.add.container(26 * i, 0); + iconContainer.setScale(0.75); + const pokemon = p.toPokemon(this.scene); + const icon = this.scene.addPokemonIcon(pokemon, 0, 0, 0, 0); + + const text = addTextObject(this.scene, 32, 20, `Lv${Utils.formatLargeNumber(pokemon.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); + text.setShadow(0, 0, null); + text.setStroke("#424242", 14); + text.setOrigin(1, 0); + + iconContainer.add(icon); + iconContainer.add(text); + + pokemonIconsContainer.add(iconContainer); + + pokemon.destroy(); + }); + + this.add(pokemonIconsContainer); + + /* + const modifiersModule = import("../modifier/modifier"); + + const modifierIconsContainer = this.scene.add.container(148, 30); + modifierIconsContainer.setScale(0.5); + let visibleModifierIndex = 0; + for (const m of data.modifiers) { + const modifier = m.toModifier(this.scene, modifiersModule[m.className]); + if (modifier instanceof PokemonHeldItemModifier) { + continue; + } + const icon = modifier.getIcon(this.scene, false); + icon.setPosition(24 * visibleModifierIndex, 0); + modifierIconsContainer.add(icon); + if (++visibleModifierIndex === 12) { + break; + } + } + + this.add(modifierIconsContainer); + */ + } +} + + +interface RunEntry { + scene: BattleScene; +} + From 1dc495cb38bd24774045e604deba57ccf63fc09a Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 07:46:01 -0700 Subject: [PATCH 02/35] Final changes --- public/images/ui/hall_of_fame.png | Bin 0 -> 1728 bytes src/phases.ts | 1118 +++++++++++------------------ src/system/game-data.ts | 465 ++++-------- src/ui/run-history-ui-handler.ts | 77 +- src/ui/run-info-ui-handler.ts | 659 +++++++++++++++++ 5 files changed, 1270 insertions(+), 1049 deletions(-) create mode 100644 public/images/ui/hall_of_fame.png create mode 100644 src/ui/run-info-ui-handler.ts diff --git a/public/images/ui/hall_of_fame.png b/public/images/ui/hall_of_fame.png new file mode 100644 index 0000000000000000000000000000000000000000..fb5f7e6cfb86c89103435e8d59a36bdcabfbf2e6 GIT binary patch literal 1728 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yow{Wll$)``g3IZv{;vjb?hIQv;UNSJSC3w0x zhE&A8y|K~vZ~%kr#rKjCOq$FA1`O6IjH^VhObl}I@}0=$c9J6`#3S=%hEelGrPQ{F zUCWE|KV!|&(6d9TkTQg2rGNj08H#evDB>87BH!^iZ;>JA{Tii7eDSx2w}2$#kR z4!nH-yL?~G+~bEYKQ1VhFL_|AF_$@gTe~DWM4fGVfY# literal 0 HcmV?d00001 diff --git a/src/phases.ts b/src/phases.ts index 9f927b0fcc1..a259917a4bc 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,11 +1,12 @@ 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 { 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 { 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 { 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, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, PokemonResetNegativeStatStageModifier } from "./modifier/modifier"; +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 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"; @@ -16,57 +17,53 @@ 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, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags"; -import { getPokemonMessage, getPokemonNameWithAffix } from "./messages"; +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 { 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 { 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 { 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 { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; -import { ChallengeAchv, HealAchv, LevelAchv, achvs } from "./system/achv"; +import { BattleSpec } from "./enums/battle-spec"; +import { Species } from "./data/enums/species"; +import { 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 { clientSessionId, loggedInUser, updateUserInfo } from "./account"; -import { SessionSaveData } from "./system/game-data"; +import { loggedInUser, updateUserInfo } from "./account"; +import { PlayerGender, SessionSaveData, saveRunHistory } from "./system/game-data"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims"; -import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; +import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler"; -import { SettingKeys } from "./system/settings/settings"; +import { Setting } from "./system/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 { GameMode, GameModes, getGameMode } from "./game-mode"; +import { GameModes, gameModes } from "./game-mode"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species"; import i18next from "./plugins/i18n"; -import Overrides from "#app/overrides"; +import { Abilities } from "./data/enums/abilities"; +import * as Overrides from "./overrides"; import { TextStyle, addTextObject } from "./ui/text"; import { Type } from "./data/type"; -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"; +import { MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./battle-scene-events"; +import { runCount } from "./ui/run-history-ui-handler"; -const { t } = i18next; export class LoginPhase extends Phase { private showText: boolean; @@ -95,14 +92,7 @@ export class LoginPhase extends Phase { this.scene.playSound("menu_open"); const loadData = () => { - updateUserInfo().then(success => { - if (!success[0]) { - Utils.removeCookie(Utils.sessionIdKey); - this.scene.reset(true, true); - return; - } - this.scene.gameData.loadSystem().then(() => this.end()); - }); + updateUserInfo().then(() => this.scene.gameData.loadSystem().then(() => this.end())); }; this.scene.ui.setMode(Mode.LOGIN_FORM, { @@ -116,36 +106,16 @@ export class LoginPhase extends Phase { buttonActions: [ () => { this.scene.ui.playSelect(); - updateUserInfo().then(success => { - if (!success[0]) { - Utils.removeCookie(Utils.sessionIdKey); - this.scene.reset(true, true); - return; - } - this.end(); - } ); + updateUserInfo().then(() => 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(); @@ -157,7 +127,7 @@ export class LoginPhase extends Phase { this.end(); } else { this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(t("menu:failedToLoadSaveData")); + this.scene.ui.showText(i18next.t("menu:failedToLoadSaveData")); } }); } @@ -178,7 +148,7 @@ export class LoginPhase extends Phase { export class TitlePhase extends Phase { private loaded: boolean; private lastSessionData: SessionSaveData; - public gameMode: GameModes; + private gameMode: GameModes; constructor(scene: BattleScene) { super(scene); @@ -212,7 +182,7 @@ export class TitlePhase extends Phase { const options: OptionSelectItem[] = []; if (loggedInUser.lastSessionSlot > -1) { options.push({ - label: i18next.t("continue", null, { ns: "menu"}), + label: i18next.t("menu:continue"), handler: () => { this.loadSaveSlot(this.lastSessionData ? -1 : loggedInUser.lastSessionSlot); return true; @@ -231,21 +201,14 @@ export class TitlePhase extends Phase { if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { const options: OptionSelectItem[] = [ { - label: GameMode.getModeName(GameModes.CLASSIC), + label: gameModes[GameModes.CLASSIC].getName(), handler: () => { setModeAndEnd(GameModes.CLASSIC); return true; } }, { - label: GameMode.getModeName(GameModes.CHALLENGE), - handler: () => { - setModeAndEnd(GameModes.CHALLENGE); - return true; - } - }, - { - label: GameMode.getModeName(GameModes.ENDLESS), + label: gameModes[GameModes.ENDLESS].getName(), handler: () => { setModeAndEnd(GameModes.ENDLESS); return true; @@ -254,7 +217,7 @@ export class TitlePhase extends Phase { ]; if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { options.push({ - label: GameMode.getModeName(GameModes.SPLICED_ENDLESS), + label: gameModes[GameModes.SPLICED_ENDLESS].getName(), handler: () => { setModeAndEnd(GameModes.SPLICED_ENDLESS); return true; @@ -300,14 +263,6 @@ 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, @@ -343,7 +298,7 @@ export class TitlePhase extends Phase { this.scene.sessionSlotId = slotId; const generateDaily = (seed: string) => { - this.scene.gameMode = getGameMode(GameModes.DAILY); + this.scene.gameMode = gameModes[GameModes.DAILY]; this.scene.setSeed(seed); this.scene.resetSeed(1); @@ -405,12 +360,7 @@ export class TitlePhase extends Phase { end(): void { if (!this.loaded && !this.scene.gameMode.isDaily) { this.scene.arena.preloadBgm(); - 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.pushPhase(new SelectStarterPhase(this.scene, this.gameMode)); this.scene.newArena(this.scene.gameMode.getStartingBiome(this.scene)); } else { this.scene.playBgm(); @@ -419,7 +369,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.isAllowedInBattle()).length; + const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()).length; this.scene.pushPhase(new SummonPhase(this.scene, 0, true, true)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -517,19 +467,19 @@ export class SelectGenderPhase extends Phase { this.scene.ui.setMode(Mode.OPTION_SELECT, { options: [ { - label: i18next.t("settings:boy"), + label: i18next.t("menu:boy"), handler: () => { this.scene.gameData.gender = PlayerGender.MALE; - this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 0); + this.scene.gameData.saveSetting(Setting.Player_Gender, 0); this.scene.gameData.saveSystem().then(() => this.end()); return true; } }, { - label: i18next.t("settings:girl"), + label: i18next.t("menu:girl"), handler: () => { this.scene.gameData.gender = PlayerGender.FEMALE; - this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 1); + this.scene.gameData.saveSetting(Setting.Player_Gender, 1); this.scene.gameData.saveSystem().then(() => this.end()); return true; } @@ -545,24 +495,13 @@ export class SelectGenderPhase extends Phase { } } -export class SelectChallengePhase extends Phase { - constructor(scene: BattleScene) { - super(scene); - } - - start() { - super.start(); - - this.scene.playBgm("menu"); - - this.scene.ui.setMode(Mode.CHALLENGE_SELECT); - } -} - export class SelectStarterPhase extends Phase { + private gameMode: GameModes; - constructor(scene: BattleScene) { + constructor(scene: BattleScene, gameMode: GameModes) { super(scene); + + this.gameMode = gameMode; } start() { @@ -579,71 +518,59 @@ export class SelectStarterPhase extends Phase { return this.end(); } this.scene.sessionSlotId = slotId; - this.initBattle(starters); + + 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(); + }); }); - }); - } - - /** - * 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(); - }); + }, this.gameMode); } } @@ -657,7 +584,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; } @@ -698,19 +625,11 @@ 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[]; - // 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) => { + let orderedTargets: Pokemon[] = playerField.concat(enemyField).sort((a: Pokemon, b: Pokemon) => { const aSpeed = a?.getBattleStat(Stat.SPD) || 0; const bSpeed = b?.getBattleStat(Stat.SPD) || 0; - return bSpeed - aSpeed; + return aSpeed < bSpeed ? 1 : aSpeed > bSpeed ? -1 : !this.scene.randBattleSeedInt(2) ? -1 : 1; }); const speedReversed = new Utils.BooleanHolder(false); @@ -814,8 +733,6 @@ 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)); @@ -869,7 +786,7 @@ export class EncounterPhase extends BattlePhase { loadEnemyAssets.push(enemyPokemon.loadAssets()); - console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats); + console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats); }); if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { @@ -879,11 +796,9 @@ 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 the enemy pokemon is a boss and wasn't populated from data source, then set it up - if (enemyPokemon.isBoss() && !enemyPokemon.isPopulatedFromDataSource) { + if (enemyPokemon.isBoss()) { enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst))); enemyPokemon.initBattleInfo(); } @@ -956,7 +871,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: () => { @@ -971,21 +886,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: getPokemonNameWithAffix(enemyField[0])}); + return i18next.t("battle:bossAppeared", {bossName: enemyField[0].name}); } 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].getNameToRender() }) - : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].getNameToRender(), pokemonName2: enemyField[1].getNameToRender() }); + ? i18next.t("battle:singleWildAppeared", {pokemonName: enemyField[0].name}) + : i18next.t("battle:multiWildAppeared", {pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name}); } doEncounterCommon(showEncounterMessage: boolean = true) { @@ -1015,7 +930,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; @@ -1037,15 +952,14 @@ 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 = () => { - this.scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { + 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.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doSummon())); }); }; - if (this.scene.currentBattle.trainer.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { + if (this.scene.currentBattle.trainer.config.hasCharSprite) { this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer.getKey(), getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon())); } else { showDialogueAndSummon(); @@ -1064,21 +978,7 @@ export class EncounterPhase extends BattlePhase { }); if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { - 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; - })); + enemyField.map(p => this.scene.pushPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()))); 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)))); @@ -1086,7 +986,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); + const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()); if (!availablePartyMembers[0].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 0)); @@ -1116,6 +1016,7 @@ export class EncounterPhase extends BattlePhase { } } } + handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end()); } @@ -1140,10 +1041,6 @@ export class NextEncounterPhase extends EncounterPhase { super(scene); } - start() { - super.start(); - } - doEncounter(): void { this.scene.playBgm(undefined, true); @@ -1158,7 +1055,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: () => { @@ -1201,7 +1098,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: () => { @@ -1223,9 +1120,6 @@ 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()); } @@ -1267,7 +1161,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); @@ -1322,7 +1216,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: () => { @@ -1340,7 +1234,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", @@ -1386,38 +1280,25 @@ export class SummonPhase extends PartyMemberPokemonPhase { */ preSummon(): void { const partyMember = this.getPokemon(); - // If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon - if (!partyMember.isAllowedInBattle()) { - 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); - } - + // 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..."); const party = this.getParty(); // Find the first non-fainted Pokemon index above the current one - const legalIndex = party.findIndex((p, i) => i > this.partyMemberIndex && p.isAllowedInBattle()); - if (legalIndex === -1) { + const nonFaintedIndex = party.findIndex((p, i) => i > this.partyMemberIndex && !p.isFainted()); + if (nonFaintedIndex === -1) { console.error("Party Details:\n", party); - console.error("All available Pokemon were fainted or illegal!"); - this.scene.clearPhaseQueue(); - this.scene.unshiftPhase(new GameOverPhase(this.scene)); - this.end(); - return; + throw new Error("All available Pokemon were fainted!"); } - // 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]); + // 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]); } if (this.player) { - this.scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(this.getPokemon()) })); + this.scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: this.getPokemon().name })); if (this.player) { this.scene.pbTray.hide(); } @@ -1437,7 +1318,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 = getPokemonNameWithAffix(this.getPokemon()); + const pokemonName = this.getPokemon().name; const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName }); this.scene.pbTrayEnemy.hide(); @@ -1456,7 +1337,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { if (this.fieldIndex === 1) { pokemon.setFieldPosition(FieldPosition.RIGHT, 0); } else { - const availablePartyMembers = this.getParty().filter(p => p.isAllowedInBattle()).length; + const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length; pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); } @@ -1534,6 +1415,7 @@ 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(); } } @@ -1556,15 +1438,6 @@ 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); @@ -1573,10 +1446,6 @@ export class SwitchSummonPhase extends SummonPhase { this.batonPass = batonPass; } - start(): void { - super.start(); - } - preSummon(): void { if (!this.player) { if (this.slotIndex === -1) { @@ -1604,10 +1473,10 @@ export class SwitchSummonPhase extends SummonPhase { } this.scene.ui.showText(this.player ? - i18next.t("battle:playerComeBack", { pokemonName: getPokemonNameWithAffix(pokemon) }) : + i18next.t("battle:playerComeBack", { pokemonName: pokemon.name }) : i18next.t("battle:trainerComeBack", { trainerName: this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER), - pokemonName: getPokemonNameWithAffix(pokemon) + pokemonName: pokemon.name }) ); this.scene.playSound("pb_rel"); @@ -1638,7 +1507,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); + this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedPokemon, false, false); } } } @@ -1647,17 +1516,12 @@ export class SwitchSummonPhase extends SummonPhase { party[this.fieldIndex] = switchedPokemon; const showTextAndSummon = () => { this.scene.ui.showText(this.player ? - i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(switchedPokemon) }) : + i18next.t("battle:playerGo", { pokemonName: switchedPokemon.name }) : i18next.t("battle:trainerGo", { trainerName: this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER), - pokemonName: getPokemonNameWithAffix(this.getPokemon()) + pokemonName: this.getPokemon().name }) ); - // 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) { @@ -1678,16 +1542,11 @@ export class SwitchSummonPhase extends SummonPhase { super.onEnd(); const pokemon = this.getPokemon(); - - const moveId = this.lastPokemon?.scene.currentBattle.lastMove; + const moveId = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.move?.move; 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 - // Or compensate for force switch move if switched out pokemon is not fainted - if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) { + 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 pokemon.battleSummonData.turnCount--; } @@ -1763,7 +1622,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.isAllowedInBattle()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { + playerPokemon.setFieldPosition(this.double && this.scene.getParty().filter(p => !p.isFainted()).length > 1 ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { if (playerPokemon.getFieldIndex() === 1) { const party = this.scene.getParty(); party[1] = party[0]; @@ -1793,11 +1652,6 @@ 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(); @@ -1814,7 +1668,7 @@ export class CheckSwitchPhase extends BattlePhase { return; } - this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => { + this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? pokemon.name : 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); @@ -1834,7 +1688,7 @@ export class SummonMissingPhase extends SummonPhase { } preSummon(): void { - this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: getPokemonNameWithAffix(this.getPokemon()) })); + this.scene.ui.showText(i18next.t("battle:sendOutPokemon", { pokemonName: this.getPokemon().name})); this.scene.time.delayedCall(250, () => this.summon()); } } @@ -1863,34 +1717,6 @@ 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()); @@ -1925,15 +1751,9 @@ export class CommandPhase extends FieldPhase { super.start(); if (this.fieldIndex) { - // 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 }; - } + 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 }; } } @@ -1947,7 +1767,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(); } @@ -1977,18 +1797,15 @@ 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]; - } - console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); - if (moveTargets.targets.length > 1 && moveTargets.multiple) { - this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); + turnCommand.targets = [ 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) { @@ -2104,7 +1921,7 @@ export class CommandPhase extends FieldPhase { } this.scene.ui.showText( i18next.t("battle:noEscapePokemon", { - pokemonName: getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)), + pokemonName: this.scene.getPokemonById(trapTag.sourceId).name, moveName: trapTag.getMoveName(), escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee") }), @@ -2207,7 +2024,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++; @@ -2238,13 +2055,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, (targets: BattlerIndex[]) => { + this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (cursor: integer) => { this.scene.ui.setMode(Mode.MESSAGE); - if (targets.length < 1) { + if (cursor === -1) { this.scene.currentBattle.turnCommands[this.fieldIndex] = null; this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); } else { - turnCommand.targets = targets; + turnCommand.targets = [ cursor ]; } if (turnCommand.command === Command.BALL && this.fieldIndex) { this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true; @@ -2269,7 +2086,6 @@ 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; }); @@ -2293,10 +2109,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) { @@ -2314,8 +2130,6 @@ 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]; @@ -2328,7 +2142,6 @@ export class TurnStartPhase extends FieldPhase { switch (turnCommand.command) { case Command.FIGHT: const queuedMove = turnCommand.move; - pokemon.turnData.order = orderIndex++; if (!queuedMove) { continue; } @@ -2360,7 +2173,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]; @@ -2375,7 +2188,9 @@ export class TurnStartPhase extends FieldPhase { } - this.scene.pushPhase(new WeatherEffectPhase(this.scene)); + if (this.scene.arena.weather) { + this.scene.pushPhase(new WeatherEffectPhase(this.scene, this.scene.arena.weather)); + } for (const o of order) { if (field[o].status && field[o].status.isPostTurn()) { @@ -2386,11 +2201,6 @@ 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(); } } @@ -2424,7 +2234,6 @@ 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()); @@ -2453,7 +2262,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: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name }))); + this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: `${getPokemonPrefix(pokemon)}${pokemon.name}`, moveName: allMoves[pokemon.summonData.disabledMove].name }))); pokemon.summonData.disabledMove = Moves.NONE; } @@ -2461,7 +2270,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), i18next.t("battle:turnEndHpRestore", { pokemonName: getPokemonNameWithAffix(pokemon) }), true)); + Math.max(pokemon.getMaxHp() >> 4, 1), getPokemonMessage(pokemon, "'s HP was restored."), true)); } if (!pokemon.isPlayer()) { @@ -2471,8 +2280,6 @@ 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++; @@ -2520,7 +2327,7 @@ export class BattleEndPhase extends BattlePhase { } } - for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { + for (const pokemon of this.scene.getParty().filter(p => !p.isFainted())) { applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); } @@ -2566,10 +2373,6 @@ 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(); @@ -2621,11 +2424,6 @@ 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(); } @@ -2645,36 +2443,22 @@ 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().hasAttr(BypassRedirectAttr))) { + if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().getAttrs(BypassRedirectAttr).length)) { //If an ability prevented this move from being redirected, display its ability pop up. - if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) { + if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().getAttrs(BypassRedirectAttr).length) && 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 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(); - } + 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(); } } if (this.targets[0] === BattlerIndex.ATTACKER) { @@ -2752,7 +2536,7 @@ export class MovePhase extends BattlePhase { this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); } - if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { + if (!allMoves[this.move.moveId].getAttrs(CopyMoveAttr).length) { this.scene.currentBattle.lastMove = this.move.moveId; } @@ -2768,16 +2552,6 @@ 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 { @@ -2792,7 +2566,7 @@ export class MovePhase extends BattlePhase { this.scene.getPlayerField().forEach(pokemon => { applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); }); - this.scene.getEnemyField().forEach(pokemon => { + this.scene.getEnemyParty().forEach(pokemon => { applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); }); } @@ -2825,12 +2599,12 @@ export class MovePhase extends BattlePhase { } if (activated) { - this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); + this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectActivationText(this.pokemon.status.effect))); 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(getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); + this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectHealText(this.pokemon.status.effect))); this.pokemon.resetStatus(); this.pokemon.updateInfo(); } @@ -2846,13 +2620,10 @@ export class MovePhase extends BattlePhase { } showMoveText(): void { - if (this.move.getMove().hasAttr(ChargeAttr)) { + if (this.move.getMove().getAttrs(ChargeAttr).length) { 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(i18next.t("battle:useMove", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - moveName: this.move.getName() - }), 500); + this.scene.queueMessage(getPokemonMessage(this.pokemon, ` used\n${this.move.getName()}!`), 500); return; } } @@ -2861,10 +2632,7 @@ export class MovePhase extends BattlePhase { return; } - this.scene.queueMessage(i18next.t("battle:useMove", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - moveName: this.move.getName() - }), 500); + this.scene.queueMessage(getPokemonMessage(this.pokemon, ` used\n${this.move.getName()}!`), 500); applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents().find(() => true), this.move.getMove()); } @@ -2893,7 +2661,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; } @@ -2909,10 +2677,9 @@ 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(), move, overridden, this.move.virtual).then(() => { + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget(), this.move.getMove(), overridden, this.move.virtual).then(() => { if (overridden.value) { return this.end(); @@ -2923,95 +2690,86 @@ 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(), move, hitCount); - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, targets.length, hitCount, new Utils.IntegerHolder(0)); - if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { + applyMoveAttrs(MultiHitAttr, user, this.getTarget(), this.move.getMove(), hitCount); + if (this.move.getMove() instanceof AttackMove && !this.move.getMove().getAttrs(FixedDamageAttr).length) { 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 || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) { - this.stopMultiHit(); + 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) { - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(this.getTarget()) })); + this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!")); moveHistoryEntry.result = MoveResult.MISS; - applyMoveAttrs(MissEffectAttr, user, null, move); + applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); } 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(move.id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => { + new MoveAnim(this.move.getMove().id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => { for (const target of targets) { if (!targetHitChecks[target.getBattlerIndex()]) { - this.stopMultiHit(target); - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; + this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!")); if (moveHistoryEntry.result === MoveResult.PENDING) { moveHistoryEntry.result = MoveResult.MISS; } - user.pushMoveHistory(moveHistoryEntry); - applyMoveAttrs(MissEffectAttr, user, null, move); + applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); continue; } - const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)); + const isProtected = !this.move.getMove().hasFlag(MoveFlags.IGNORE_PROTECT) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)); - const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); - const firstTarget = (moveHistoryEntry.result === MoveResult.PENDING); - - if (firstHit) { - user.pushMoveHistory(moveHistoryEntry); - } + const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS; moveHistoryEntry.result = MoveResult.SUCCESS; - const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; + const hitResult = !isProtected ? target.apply(user, this.move) : HitResult.NO_EFFECT; - const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); - - if (lastHit) { - this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); - } + this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); applyAttrs.push(new Promise(resolve => { - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), - user, target, move).then(() => { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit), + user, target, this.move.getMove()).then(() => { if (hitResult !== HitResult.FAIL) { - const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move)); + const chargeEffect = !!this.move.getMove().getAttrs(ChargeAttr).find(ca => (ca as ChargeAttr).usedChargeEffect(user, this.getTarget(), this.move.getMove())); // 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.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { + 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(() => { 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) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { - if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { + && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit), user, target, this.move.getMove()).then(() => { + if (hitResult < HitResult.NO_EFFECT) { 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) && (!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(() => { + 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(() => { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); } })).then(() => { - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { + applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move, hitResult).then(() => { if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); } resolve(); }); @@ -3020,7 +2778,7 @@ export class MoveEffectPhase extends PokemonPhase { ).then(() => resolve()); }); } else { - applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); + applyMoveAttrs(NoEffectAttr, user, null, this.move.getMove()).then(() => resolve()); } }); } else { @@ -3029,17 +2787,14 @@ export class MoveEffectPhase extends PokemonPhase { }); })); } - // 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; + // 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()); - 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); - } + 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()); @@ -3048,18 +2803,13 @@ 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 || user.turnData.hitsLeft > 0) { - // If there are multiple hits, or if there are hits of the multi-hit move left + if (hitsTotal > 1) { this.scene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); } this.scene.applyModifiers(HitHealModifier, this.player, user); @@ -3077,13 +2827,9 @@ export class MoveEffectPhase extends PokemonPhase { const user = this.getUserPokemon(); - // 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 + // Hit check only calculated on first hit for multi-hit moves if (user.turnData.hitsLeft < user.turnData.hitCount) { - if (!this.move.getMove().hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) { - return true; - } + return true; } if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { @@ -3091,29 +2837,62 @@ 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().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().slice(1).find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { return true; } - 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)) { + const hiddenTag = target.getTag(HiddenTag); + if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).filter(hta => (hta as HitsTagAttr).tagType === hiddenTag.tagType).length) { return false; } - const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user, target); + const moveAccuracy = new Utils.NumberHolder(this.move.getMove().accuracy); - if (moveAccuracy === -1) { + applyMoveAttrs(VariableAccuracyAttr, user, target, this.move.getMove(), moveAccuracy); + + if (moveAccuracy.value === -1) { return true; } - const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove()); + 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 rand = user.randSeedInt(100, 1); - return rand <= moveAccuracy * accuracyMultiplier; + 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; } getUserPokemon(): Pokemon { @@ -3131,28 +2910,6 @@ 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); } @@ -3201,7 +2958,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) { @@ -3227,19 +2984,12 @@ export class ShowAbilityPhase extends PokemonPhase { start() { super.start(); - const pokemon = this.getPokemon(); - - this.scene.abilityBar.showAbility(pokemon, this.passive); - if (pokemon.battleData) { - pokemon.battleData.abilityRevealed = true; - } + this.scene.abilityBar.showAbility(this.getPokemon(), this.passive); this.end(); } } -export type StatChangeCallback = (target: Pokemon, changed: BattleStat[], relativeChanges: number[]) => void; - export class StatChangePhase extends PokemonPhase { private stats: BattleStat[]; private selfTarget: boolean; @@ -3247,10 +2997,8 @@ 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, onChange: StatChangeCallback = null) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true) { super(scene, battlerIndex); this.selfTarget = selfTarget; @@ -3259,7 +3007,6 @@ export class StatChangePhase extends PokemonPhase { this.showMessage = showMessage; this.ignoreAbilities = ignoreAbilities; this.canBeCopied = canBeCopied; - this.onChange = onChange; } start() { @@ -3301,8 +3048,6 @@ 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); @@ -3323,21 +3068,6 @@ 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()); @@ -3352,10 +3082,7 @@ export class StatChangePhase extends PokemonPhase { const tileWidth = 156 * this.scene.field.scale * pokemon.getSpriteScale(); const tileHeight = 316 * this.scene.field.scale * pokemon.getSpriteScale(); - // 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); + const statSprite = this.scene.add.tileSprite(tileX, tileY, tileWidth, tileHeight, "battle_stats", filteredStats.length > 1 ? "mix" : BattleStat[filteredStats[0]].toLowerCase()); statSprite.setPipeline(this.scene.fieldSpritePipeline); statSprite.setAlpha(0); statSprite.setScale(6); @@ -3400,7 +3127,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 @@ -3420,7 +3147,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)) { @@ -3447,13 +3174,12 @@ export class StatChangePhase extends PokemonPhase { if (relLevelStats.length > 1) { statsFragment = relLevelStats.length >= 5 - ? 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)); + ? "stats" + : `${relLevelStats.slice(0, -1).map(s => getBattleStatName(s)).join(", ")}${relLevelStats.length > 2 ? "," : ""} and ${getBattleStatName(relLevelStats[relLevelStats.length - 1])}`; } 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; @@ -3463,22 +3189,12 @@ export class StatChangePhase extends PokemonPhase { export class WeatherEffectPhase extends CommonAnimPhase { public weather: Weather; - constructor(scene: BattleScene) { - super(scene, undefined, undefined, CommonAnim.SUNNY + ((scene?.arena?.weather?.weatherType || WeatherType.NONE) - 1)); - this.weather = scene?.arena?.weather; + constructor(scene: BattleScene, weather: Weather) { + super(scene, undefined, undefined, CommonAnim.SUNNY + (weather.weatherType - 1)); + this.weather = 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); @@ -3543,7 +3259,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { } pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(this.scene, () => { - this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText)); + this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectObtainText(this.statusEffect, this.sourceText))); if (pokemon.status.isPostTurn()) { this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); } @@ -3552,7 +3268,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { return; } } else if (pokemon.status.effect === this.statusEffect) { - this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon))); + this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectOverlapText(this.statusEffect))); } this.end(); } @@ -3569,10 +3285,9 @@ 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(getStatusEffectActivationText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); + this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectActivationText(pokemon.status.effect))); let damage: integer = 0; switch (pokemon.status.effect) { case StatusEffect.POISON: @@ -3586,7 +3301,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(); } @@ -3598,14 +3313,6 @@ 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 { @@ -3661,9 +3368,7 @@ export class DamagePhase extends PokemonPhase { super.start(); if (this.damageResult === HitResult.ONE_HIT_KO) { - if (this.scene.moveAnimations) { - this.scene.toggleInvert(true); - } + this.scene.toggleInvert(true); this.scene.time.delayedCall(Utils.fixedInt(1000), () => { this.scene.toggleInvert(false); this.applyDamage(); @@ -3713,12 +3418,34 @@ export class DamagePhase extends PokemonPhase { } } - override end() { - if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - this.scene.initFinalBossPhaseTwo(this.getPokemon()); - } else { - super.end(); + 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; } + + super.end(); } } @@ -3754,18 +3481,11 @@ export class FaintPhase extends PokemonPhase { doFaint(): void { const pokemon = this.getPokemon(); - // 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); + this.scene.queueMessage(getPokemonMessage(pokemon, " fainted!"), 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).getMove(), lastAttack.result); + applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, this.scene.getPokemonById(lastAttack.sourceId), new PokemonMove(lastAttack.move), lastAttack.result); } const alivePlayField = this.scene.getField(true); @@ -3775,7 +3495,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); + const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr) as PostVictoryStatChangeAttr[]; if (pvattrs.length) { for (const pvattr of pvattrs) { pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); @@ -3785,11 +3505,11 @@ export class FaintPhase extends PokemonPhase { } if (this.player) { - const nonFaintedLegalPartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()); - const nonFaintedPartyMemberCount = nonFaintedLegalPartyMembers.length; + const nonFaintedPartyMembers = this.scene.getParty().filter(p => !p.isFainted()); + const nonFaintedPartyMemberCount = nonFaintedPartyMembers.length; if (!nonFaintedPartyMemberCount) { this.scene.unshiftPhase(new GameOverPhase(this.scene)); - } else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedLegalPartyMembers[0].isActive(true))) { + } else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !nonFaintedPartyMembers[0].isActive(true))) { this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); } if (nonFaintedPartyMemberCount === 1 && this.scene.currentBattle.double) { @@ -3923,9 +3643,7 @@ export class VictoryPhase extends PokemonPhase { expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; } const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); - const modifierBonusExp = new Utils.NumberHolder(1); - this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, modifierBonusExp); - pokemonExp.value *= modifierBonusExp.value; + this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); partyMemberExp.push(Math.floor(pokemonExp.value)); } @@ -4026,24 +3744,27 @@ 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(); - let message: string; - this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(victoryMessages), this.scene.currentBattle.waveIndex); - const showMessage = () => { - const originalFunc = showMessageOrEnd; - showMessageOrEnd = () => this.scene.ui.showDialogue(message, this.scene.currentBattle.trainer.getName(), null, originalFunc); + let message: string; + this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(victoryMessages), this.scene.currentBattle.waveIndex); + const messagePages = message.split(/\$/g).map(m => m.trim()); + + 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); + } showMessageOrEnd(); }; let showMessageOrEnd = () => this.end(); if (victoryMessages?.length) { - if (this.scene.currentBattle.trainer.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { + if (this.scene.currentBattle.trainer.config.hasCharSprite) { 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())); @@ -4073,10 +3794,6 @@ 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"; @@ -4107,7 +3824,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(i18next.t("battle:rewardGain", { modifierName: newModifier.type.name }), null, () => resolve(), null, true); + this.scene.ui.showText(`You received\n${newModifier.type.name}!`, null, () => resolve(), null, true); }); }); } @@ -4125,7 +3842,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(i18next.t("battle:rewardGain", { modifierName: newModifier.type.name }), null, () => { + this.scene.ui.showText(`You received\n${newModifier.type.name}!`, null, () => { this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); resolve(); }, null, true, 1500); @@ -4150,11 +3867,7 @@ 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(i18next.t("battle:beatModeFirstTime", { - speciesName: this.species.name, - gameMode: this.scene.gameMode.getName(), - newModifier: newModifier.type.name - }), null, () => { + this.scene.ui.showText(`${this.species.name} beat ${this.scene.gameMode.getName()} Mode for the first time!\nYou received ${newModifier.type.name}!`, null, () => { resolve(); }, null, true, 1500); }); @@ -4185,7 +3898,7 @@ export class GameOverPhase extends BattlePhase { } else if (this.victory || !this.scene.enableRetries) { this.handleGameOver(); } else { - this.scene.ui.showText(i18next.t("battle:retryBattle"), null, () => { + this.scene.ui.showText("Would you like to retry from the start of the battle?", null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.fadeOut(1250).then(() => { this.scene.reset(); @@ -4193,7 +3906,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.isAllowedInBattle()).length; + const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()).length; this.scene.pushPhase(new SummonPhase(this.scene, 0)); if (this.scene.currentBattle.double && availablePartyMembers > 1) { @@ -4235,6 +3948,15 @@ 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); + } + }).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)); @@ -4245,10 +3967,6 @@ 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(); @@ -4266,27 +3984,19 @@ export class GameOverPhase extends BattlePhase { }; if (this.victory && this.scene.gameMode.isClassic) { - 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); - }); + 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); }); }); }); }); - } else { - const endCardPhase = new EndCardPhase(this.scene); - this.scene.unshiftPhase(endCardPhase); - clear(endCardPhase); - } + }); } else { clear(); } @@ -4299,7 +4009,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/session/newclear?slot=${this.scene.sessionSlotId}&clientSessionId=${clientSessionId}`, true) + Utils.apiFetch(`savedata/newclear?slot=${this.scene.sessionSlotId}`, true) .then(response => response.json()) .then(newClear => doGameOver(newClear)); } else { @@ -4355,7 +4065,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, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" }); + 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.setOrigin(0.5); this.scene.field.add(this.text); @@ -4385,7 +4095,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(i18next.t("battle:unlockedSomething", { unlockedThing: getUnlockableName(this.unlockable) }), null, () => { + this.scene.ui.showText(`${getUnlockableName(this.unlockable)}\nhas been unlocked.`, null, () => { this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); this.end(); }, null, true, 1500); @@ -4452,17 +4162,17 @@ export class SwitchPhase extends BattlePhase { super.start(); // Skip modal switch if impossible - if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { + if (this.isModal && !this.scene.getParty().filter(p => !p.isFainted() && !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.isAllowedInBattle() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) { + if (this.isModal && this.scene.getPlayerField().filter(p => !p.isFainted() && 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 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; + // 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; 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) { @@ -4489,7 +4199,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: getPokemonNameWithAffix(pokemon), exp: exp.value }), null, () => { + this.scene.ui.showText(i18next.t("battle:expGain", { pokemonName: pokemon.name, exp: exp.value }), null, () => { const lastLevel = pokemon.level; pokemon.addExp(exp.value); const newLevel = pokemon.level; @@ -4527,20 +4237,20 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { this.scene.unshiftPhase(new HidePartyExpBarPhase(this.scene)); pokemon.updateInfo(); - if (this.scene.expParty === ExpNotification.SKIP) { + if (this.scene.expParty === 2) { // 2 - Skip - no level up frame nor message this.end(); - } else if (this.scene.expParty === ExpNotification.ONLY_LEVEL_UP) { + } else if (this.scene.expParty === 1) { // 1 - Only level up - we display the level up in the small frame instead of a message 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 === ExpNotification.ONLY_LEVEL_UP, newLevel).then(() => { + this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, 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, false, newLevel).then(() => { + this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, newLevel).then(() => { setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed)); }); } else { @@ -4587,16 +4297,16 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { const prevStats = pokemon.stats.slice(0); pokemon.calculateStats(); pokemon.updateInfo(); - if (this.scene.expParty === ExpNotification.DEFAULT) { + if (this.scene.expParty === 0) { // 0 - default - the normal exp gain display, nothing changed this.scene.playSound("level_up_fanfare"); - 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.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.end(); - } else { + } else { // 1 - Only level up - we display the level up in the small frame instead of a message // we still want to display the stats if activated this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()); } - if (this.lastLevel < 100) { // this feels like an unnecessary optimization + if (this.level <= 100) { const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); for (const lm of levelMoves) { this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1])); @@ -4643,11 +4353,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: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => { + this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: pokemon.name, moveName: move.name }), null, () => { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true); this.end(); }, messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true); @@ -4656,15 +4366,15 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { }); } else { this.scene.ui.setMode(messageMode).then(() => { - 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: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: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: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => this.end(), null, true); + this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: pokemon.name, moveName: move.name }), null, () => this.end(), null, true); }, () => { this.scene.ui.setMode(messageMode); this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId)); @@ -4683,7 +4393,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: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex].getName() }), 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:learnMoveAnd"), null, () => { pokemon.setMove(moveIndex, Moves.NONE); this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId)); @@ -4725,7 +4435,7 @@ export class PokemonHealPhase extends CommonAnimPhase { } start() { - if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) { + if (!this.skipAnim && (this.revive || this.getPokemon().hp) && this.getPokemon().getHpRatio() < 1) { super.start(); } else { this.end(); @@ -4740,11 +4450,12 @@ 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 (healOrDamage) { + if (!fullHp || this.hpHealed < 0) { const hpRestoreMultiplier = new Utils.IntegerHolder(1); if (!this.revive) { this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); @@ -4778,7 +4489,7 @@ export class PokemonHealPhase extends CommonAnimPhase { pokemon.resetStatus(); pokemon.updateInfo().then(() => super.end()); } else if (this.showFullHpMessage) { - this.message = i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(pokemon) }); + this.message = getPokemonMessage(pokemon, "'s\nHP is full!"); } if (this.message) { @@ -4786,10 +4497,10 @@ export class PokemonHealPhase extends CommonAnimPhase { } if (this.healStatus && lastStatusEffect && !hasMessage) { - this.scene.queueMessage(getStatusEffectHealText(lastStatusEffect, getPokemonNameWithAffix(pokemon))); + this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectHealText(lastStatusEffect))); } - if (!healOrDamage && !lastStatusEffect) { + if (fullHp && !lastStatusEffect) { super.end(); } } @@ -4919,9 +4630,7 @@ export class AttemptCapturePhase extends PokemonPhase { }); } }, - onComplete: () => { - this.catch(); - } + onComplete: () => this.catch() }); }; @@ -4962,6 +4671,7 @@ 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(); @@ -4985,9 +4695,8 @@ export class AttemptCapturePhase extends PokemonPhase { this.scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); - this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { + this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { const end = () => { - this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); this.scene.pokemonInfoContainer.hide(); this.removePb(); this.end(); @@ -5016,19 +4725,12 @@ 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: getPokemonNameWithAffix(pokemon) }), null, () => { - this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); + this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { + this.scene.pokemonInfoContainer.makeRoomForConfirmUi(); 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) { @@ -5043,7 +4745,7 @@ export class AttemptCapturePhase extends PokemonPhase { removePokemon(); end(); }); - }, "fullParty"); + }); }); }; promptRelease(); @@ -5087,7 +4789,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", @@ -5128,8 +4830,6 @@ export class SelectModifierPhase extends BattlePhase { if (!this.rerollCount) { this.updateSeed(); - } else { - this.scene.reroll = false; } const party = this.scene.getParty(); @@ -5155,41 +4855,33 @@ export class SelectModifierPhase extends BattlePhase { let cost: integer; switch (rowCursor) { case 0: - switch (cursor) { - case 0: + if (!cursor) { const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); if (this.scene.money < rerollCost) { this.scene.ui.playError(); return false; } else { - this.scene.reroll = true; this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type.tier))); this.scene.ui.clearText(); this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); this.scene.money -= rerollCost; this.scene.updateMoneyText(); - this.scene.animateMoneyChanged(false); this.scene.playSound("buy"); } - break; - case 1: - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { + } else if (cursor === 1) { + this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { - 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); + 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); + }); } else { this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); } }, PartyUiHandler.FilterItemMaxStacks); - break; - case 2: - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => { - this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); - }); - break; - case 3: + } else { this.scene.lockModifierTiers = !this.scene.lockModifierTiers; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); @@ -5221,7 +4913,6 @@ 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 { @@ -5302,7 +4993,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]; } @@ -5334,19 +5025,14 @@ export class EggLapsePhase extends Phase { super.start(); const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => { - return Overrides.EGG_IMMEDIATE_HATCH_OVERRIDE ? true : --egg.hatchWaves < 1; + return Overrides.IMMEDIATE_HATCH_EGGS_OVERRIDE ? true : --egg.hatchWaves < 1; }); - let eggCount: integer = eggsToHatch.length; - - if (eggCount) { + if (eggsToHatch.length) { this.scene.queueMessage(i18next.t("battle:eggHatching")); for (const egg of eggsToHatch) { - this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg, eggCount)); - if (eggCount > 0) { - eggCount--; - } + this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg)); } } @@ -5486,7 +5172,7 @@ export class ScanIvsPhase extends PokemonPhase { const pokemon = this.getPokemon(); - this.scene.ui.showText(i18next.t("battle:ivScannerUseQuestion", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { + this.scene.ui.showText(i18next.t("battle:ivScannerUseQuestion", { pokemonName: pokemon.name }), null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.clearText(); @@ -5522,7 +5208,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 aa04bea5170..d7c796ab339 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 Overrides from "#app/overrides"; +import * as Overrides from "../overrides"; import PokemonData from "./pokemon-data"; import PersistentModifierData from "./modifier-data"; import ArenaData from "./arena-data"; import { Unlockables } from "./unlockables"; -import { GameModes, getGameMode } from "../game-mode"; +import { GameModes, gameModes } from "../game-mode"; import { BattleType } from "../battle"; import TrainerData from "./trainer-data"; import { trainerConfigs } from "../data/trainer-config"; -import { SettingKeys, resetSettings, setSetting } from "./settings/settings"; +import { Setting, setSetting, settingDefaults } from "./settings"; import { achvs } from "./achv"; import EggData from "./egg-data"; import { Egg } from "../data/egg"; @@ -24,39 +24,34 @@ 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: @@ -71,18 +66,18 @@ 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"; } } -export function encrypt(data: string, bypassLogin: boolean): string { +function encrypt(data: string, bypassLogin: boolean): string { return (bypassLogin ? (data: string) => btoa(data) : (data: string) => AES.encrypt(data, saveKey))(data); } -export function decrypt(data: string, bypassLogin: boolean): string { +function decrypt(data: string, bypassLogin: boolean): string { return (bypassLogin ? (data: string) => atob(data) : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data); @@ -95,6 +90,7 @@ interface SystemSaveData { dexData: DexData; starterData: StarterData; gameStats: GameStats; + runHistory: RunHistoryData; unlocks: Unlocks; achvUnlocks: AchvUnlocks; voucherUnlocks: VoucherUnlocks; @@ -102,8 +98,6 @@ interface SystemSaveData { eggs: EggData[]; gameVersion: string; timestamp: integer; - eggPity: integer[]; - unlockPity: integer[]; } export interface SessionSaveData { @@ -123,7 +117,15 @@ export interface SessionSaveData { trainer: TrainerData; gameVersion: string; timestamp: integer; - challenges: ChallengeData[]; +} + +export interface RunHistoryData { + [key: integer]: RunEntries; +} + +export interface RunEntries { + entry: SessionSaveData; + victory: boolean; } interface Unlocks { @@ -139,7 +141,7 @@ interface VoucherUnlocks { } export interface VoucherCounts { - [type: string]: integer; + [type: string]: integer; } export interface DexData { @@ -190,46 +192,6 @@ 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; @@ -249,10 +211,6 @@ export interface TutorialFlags { [key: string]: boolean } -export interface SeenDialogues { - [key: string]: boolean; -} - const systemShortKeys = { seenAttr: "$sa", caughtAttr: "$ca", @@ -285,7 +243,7 @@ export class GameData { public starterData: StarterData; public gameStats: GameStats; - + public runHistory: RunHistoryData; public unlocks: Unlocks; public achvUnlocks: AchvUnlocks; @@ -293,17 +251,14 @@ 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, @@ -319,8 +274,6 @@ export class GameData { [VoucherType.GOLDEN]: 0 }; this.eggs = []; - this.eggPity = [0, 0, 0, 0]; - this.unlockPity = [0, 0, 0, 0]; this.initDexData(); this.initStarterData(); } @@ -330,6 +283,7 @@ export class GameData { trainerId: this.trainerId, secretId: this.secretId, gender: this.gender, + runHistory: this.runHistory, dexData: this.dexData, starterData: this.starterData, gameStats: this.gameStats, @@ -339,9 +293,7 @@ export class GameData { voucherCounts: this.voucherCounts, eggs: this.eggs.map(e => new EggData(e)), gameVersion: this.scene.game.config.gameVersion, - timestamp: new Date().getTime(), - eggPity: this.eggPity.slice(0), - unlockPity: this.unlockPity.slice(0) + timestamp: new Date().getTime() }; } @@ -356,7 +308,7 @@ export class GameData { localStorage.setItem(`data_${loggedInUser.username}`, encrypt(systemData, bypassLogin)); if (!bypassLogin) { - Utils.apiPost(`savedata/system/update?clientSessionId=${clientSessionId}`, systemData, undefined, true) + Utils.apiPost(`savedata/update?datatype=${GameDataType.SYSTEM}&clientSessionId=${clientSessionId}`, systemData, undefined, true) .then(response => response.text()) .then(error => { this.scene.ui.savingIcon.hide(); @@ -390,7 +342,7 @@ export class GameData { } if (!bypassLogin) { - Utils.apiFetch(`savedata/system/get?clientSessionId=${clientSessionId}`, true) + Utils.apiFetch(`savedata/system?clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== "{") { @@ -434,6 +386,11 @@ 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]) { @@ -445,7 +402,7 @@ export class GameData { this.gender = systemData.gender; - this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); const initStarterData = !systemData.starterData; @@ -524,9 +481,6 @@ 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; @@ -550,7 +504,7 @@ export class GameData { }); } - parseSystemData(dataStr: string): SystemSaveData { + private parseSystemData(dataStr: string): SystemSaveData { return JSON.parse(dataStr, (k: string, v: any) => { if (k === "gameStats") { return new GameStats(v); @@ -569,13 +523,11 @@ export class GameData { }) as SystemSaveData; } - convertSystemDataStr(dataStr: string, shorten: boolean = false): string { + private 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) { @@ -590,7 +542,7 @@ export class GameData { return true; } - const response = await Utils.apiFetch(`savedata/system/verify?clientSessionId=${clientSessionId}`, true) + const response = await Utils.apiPost("savedata/system/verify", JSON.stringify({ clientSessionId: clientSessionId }), undefined, true) .then(response => response.json()); if (!response.valid) { @@ -613,123 +565,27 @@ export class GameData { } } - /** - * 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 { + public saveSetting(setting: Setting, valueIndex: integer): boolean { let settings: object = {}; if (localStorage.hasOwnProperty("settings")) { settings = JSON.parse(localStorage.getItem("settings")); } - setSetting(this.scene, setting, valueIndex); + setSetting(this.scene, setting as Setting, valueIndex); - settings[setting] = valueIndex; + Object.keys(settingDefaults).forEach(s => { + if (s === setting) { + settings[s] = 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 { - resetSettings(this.scene); + Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); if (!localStorage.hasOwnProperty("settings")) { return false; @@ -738,28 +594,14 @@ export class GameData { const settings = JSON.parse(localStorage.getItem("settings")); for (const setting of Object.keys(settings)) { - 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]); + setSetting(this.scene, setting as Setting, settings[setting]); } } public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { - const key = getDataTypeKey(GameDataType.TUTORIALS); let tutorials: object = {}; - if (localStorage.hasOwnProperty(key)) { - tutorials = JSON.parse(localStorage.getItem(key)); + if (localStorage.hasOwnProperty("tutorials")) { + tutorials = JSON.parse(localStorage.getItem("tutorials")); } Object.keys(Tutorial).map(t => t as Tutorial).forEach(t => { @@ -771,21 +613,20 @@ export class GameData { } }); - localStorage.setItem(key, JSON.stringify(tutorials)); + localStorage.setItem("tutorials", 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(key)) { + if (!localStorage.hasOwnProperty("tutorials")) { return ret; } - const tutorials = JSON.parse(localStorage.getItem(key)); + const tutorials = JSON.parse(localStorage.getItem("tutorials")); for (const tutorial of Object.keys(tutorials)) { ret[tutorial] = tutorials[tutorial]; @@ -794,34 +635,6 @@ 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, @@ -839,8 +652,7 @@ 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(), - challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)) + timestamp: new Date().getTime() } as SessionSaveData; } @@ -852,14 +664,6 @@ 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); @@ -868,7 +672,7 @@ export class GameData { }; if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser.username}`)) { - Utils.apiFetch(`savedata/session/get?slot=${slotId}&clientSessionId=${clientSessionId}`, true) + Utils.apiFetch(`savedata/session?slot=${slotId}&clientSessionId=${clientSessionId}`, true) .then(response => response.text()) .then(async response => { if (!response.length || response[0] !== "{") { @@ -894,13 +698,10 @@ export class GameData { loadSession(scene: BattleScene, slotId: integer, sessionData?: SessionSaveData): Promise { return new Promise(async (resolve, reject) => { try { - const initSessionFromData = async (sessionData: SessionSaveData) => { + const initSessionFromData = async sessionData => { console.debug(sessionData); - scene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); - if (sessionData.challenges) { - scene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); - } + scene.gameMode = gameModes[sessionData.gameMode || GameModes.CLASSIC]; scene.setSeed(sessionData.seed || scene.game.config.seed[0]); scene.resetSeed(); @@ -959,10 +760,6 @@ 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; @@ -1016,7 +813,7 @@ export class GameData { if (success !== null && !success) { return resolve(false); } - Utils.apiFetch(`savedata/session/delete?slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { + Utils.apiFetch(`savedata/delete?datatype=${GameDataType.SESSION}&slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { if (response.ok) { loggedInUser.lastSessionSlot = -1; localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ""}_${loggedInUser.username}`); @@ -1080,7 +877,7 @@ export class GameData { return resolve([false, false]); } const sessionData = this.getSessionSaveData(scene); - Utils.apiPost(`savedata/session/clear?slot=${slotId}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, JSON.stringify(sessionData), undefined, true).then(response => { + Utils.apiPost(`savedata/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}`); @@ -1101,7 +898,7 @@ export class GameData { }); } - public parseSessionData(dataStr: string): SessionSaveData { + parseSessionData(dataStr: string): SessionSaveData { return JSON.parse(dataStr, (k: string, v: any) => { /*const versions = [ scene.game.config.gameVersion, sessionData.gameVersion || '0.0.0' ]; @@ -1134,9 +931,6 @@ 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; @@ -1146,21 +940,65 @@ 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; } + async public getRunHistoryData(scene: BattleScene): Promise { + try { + const response = await Utils.apiFetch("savedata/runHistory", true); + const data = await response.json(); + console.log(data); + if (!data) { + throw new Error("No data"); + } else { + var 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; + } + return data; + } + } catch (err) { + console.log("Something went wrong: ", err); + var cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + if (cachedResponse) { + cachedResponse = JSON.parse(decrypt(cachedResponse, true)); + } + return cachedResponse ?? {}; + } + } + + async saveRunHistory(scene: BattleScene, runEntry : SessionSaveData, victory: boolean): Promise { + + const runHistoryData = await this.getRunHistoryData(scene); + 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)); + + try { + const response = 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 => { @@ -1237,7 +1075,7 @@ export class GameData { link.remove(); }; if (!bypassLogin && dataType < GameDataType.SETTINGS) { - Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}/get?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) + Utils.apiFetch(`savedata/${dataType === GameDataType.SYSTEM ? "system" : "session"}?clientSessionId=${clientSessionId}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}`, true) .then(response => response.text()) .then(response => { if (!response.length || response[0] !== "{") { @@ -1278,11 +1116,9 @@ 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); @@ -1302,28 +1138,38 @@ 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[0]) { + if (!success) { return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`); } - 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) + Utils.apiPost(`savedata/update?datatype=${dataType}${dataType === GameDataType.SESSION ? `&slot=${slotId}` : ""}&trainerId=${this.trainerId}&secretId=${this.secretId}&clientSessionId=${clientSessionId}`, dataStr, undefined, true) .then(response => response.text()) .then(error => { if (error) { @@ -1498,7 +1344,7 @@ export class GameData { if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) { this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(), null, true); + this.scene.ui.showText(`${species.name} has been\nadded as a starter!`, null, () => checkPrevolution(), null, true); } else { checkPrevolution(); } @@ -1564,9 +1410,7 @@ export class GameData { this.starterData[speciesId].eggMoves |= value; this.scene.playSound("level_up_fanfare"); - - 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); + this.scene.ui.showText(`${eggMoveIndex === 3 ? "Rare " : ""}Egg Move unlocked: ${allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name}`, null, () => resolve(true), null, true); }); } @@ -1688,10 +1532,7 @@ export class GameData { value = decrementValue(value); } - const cost = new Utils.NumberHolder(value); - applyChallenges(this.scene.gameMode, ChallengeType.STARTER_COST, speciesId, cost); - - return cost.value; + return value; } getFormIndex(attr: bigint): integer { diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index edca87b9e74..05f53c9bf1c 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -1,12 +1,12 @@ import BattleScene from "../battle-scene"; -import { gameModes } from "../game-mode"; -import { SessionSaveData, parseSessionData, RunHistoryData, RunEntries } from "../system/game-data"; +import { gameModes, GameModes } from "../game-mode"; +import { SessionSaveData, parseSessionData, getRunHistoryData, RunHistoryData, RunEntries, decrypt } from "../system/game-data"; import { TextStyle, addTextObject } from "./text"; import { Mode } from "./ui"; import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; import { PokemonData } from "../system/pokemon-data"; -import { TrainerData } from "../system/trainer-data" +import { TrainerData } from "../system/trainer-data"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "../field/pokemon"; import { PokemonHeldItemModifier } from "../modifier/modifier"; import MessageUiHandler from "./message-ui-handler"; @@ -14,6 +14,8 @@ import i18next from "i18next"; import {Button} from "../enums/buttons"; import { BattleType } from "../battle"; import {TrainerType} from "../data/enums/trainer-type"; +import { TrainerVariant } from "../field/trainer"; +import { getPartyLuckValue, getLuckString, getLuckTextTint } from "../modifier/modifier-type"; export const runCount = 25; @@ -76,7 +78,7 @@ export default class RunHistoryUiHandler extends MessageUiHandler { this.getUi().bringToTop(this.runSelectContainer); this.runSelectContainer.setVisible(true); - this.populateruns(); + this.populateruns(this.scene); this.setScrollCursor(0); this.setCursor(0); @@ -130,15 +132,16 @@ export default class RunHistoryUiHandler extends MessageUiHandler { } - populateruns() { - const timestamps = Object.keys(this.scene.gameData.runHistory); + async populateruns(scene: BattleScene) { + const response = await this.scene.gameData.getRunHistoryData(this.scene); + console.log(response); + const timestamps = Object.keys(response); if (timestamps.length > 1) { timestamps.sort((a, b) => a - b); } const entryCount = timestamps.length; - console.log(entryCount); for (let s = 0; s < entryCount; s++) { - const entry = new RunEntry(this.scene, timestamps[s], s); + const entry = new RunEntry(this.scene, response, timestamps[s], s); this.scene.add.existing(entry); this.runsContainer.add(entry); this.runs.push(entry); @@ -215,12 +218,12 @@ class RunEntry extends Phaser.GameObjects.Container { public hasData: boolean; private loadingLabel: Phaser.GameObjects.Text; - constructor(scene: BattleScene, timestamp: string, slotId: integer) { + constructor(scene: BattleScene, runHistory: RunHistoryData, timestamp: string, slotId: integer) { super(scene, 0, slotId*56); this.slotId = slotId; - this.setup(this.scene.gameData.runHistory[timestamp]); + this.setup(runHistory[timestamp]); } @@ -228,7 +231,6 @@ class RunEntry extends Phaser.GameObjects.Container { const victory = run.victory; const data = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); - console.log(data); const slotWindow = addWindow(this.scene, 0, 0, 304, 52); this.add(slotWindow); @@ -238,13 +240,15 @@ class RunEntry extends Phaser.GameObjects.Container { const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Victory", TextStyle.WINDOW); this.add(gameOutcomeLabel); } else { - if (data.battleType === BattleType.WILD) { - const enemyContainer = this.scene.add.container(8,5); + if (data.battleType === BattleType.WILD) { + const enemyContainer = this.scene.add.container(8, 5); const gameOutcomeLabel = addTextObject(this.scene, 0, 0, "Defeated by ", TextStyle.WINDOW); enemyContainer.add(gameOutcomeLabel); - const enemyIconContainer = this.scene.add.container(58,-8); - enemyIconContainer.setScale(0.75); data.enemyParty.forEach((enemyData, e) => { + //This allows the enemyParty to be shown - doubles or sings -> 58+(e*8) + const enemyIconContainer = this.scene.add.container(65+(e*25),-8); + enemyIconContainer.setScale(0.75); + enemyData.boss = false; const enemy = enemyData.toPokemon(this.scene); const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); const enemyLevel = addTextObject(this.scene, 32, 20, `Lv${Utils.formatLargeNumber(enemy.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); @@ -257,22 +261,33 @@ class RunEntry extends Phaser.GameObjects.Container { enemy.destroy(); }); this.add(enemyContainer); - } - else if (data.battleType === BattleType.TRAINER) { + } else if (data.battleType === BattleType.TRAINER) { const tObj = data.trainer.toTrainer(this.scene); const tType = TrainerType[data.trainer.trainerType]; - const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `Defeated by ${tType.charAt(0)+tType.substring(1).toLowerCase()}`, TextStyle.WINDOW); - this.add(gameOutcomeLabel); - } + if (data.trainer.trainerType >= 375) { + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Defeated by Rival", TextStyle.WINDOW); + //otherwise it becomes Rival_5 in Ivy's case + this.add(gameOutcomeLabel); + } else { + if (tObj.variant === TrainerVariant.DOUBLE) { + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Defeated by Duo", TextStyle.WINDOW); + } + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `Defeated by ${tObj.getName(0, true)}`, TextStyle.WINDOW); + this.add(gameOutcomeLabel); + } } + } const gameModeLabel = addTextObject(this.scene, 8, 19, `${gameModes[data.gameMode]?.getName() || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); this.add(gameModeLabel); + const date = new Date(data.timestamp); + const timestampLabel = addTextObject(this.scene, 8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); this.add(timestampLabel); - const pokemonIconsContainer = this.scene.add.container(144, 4); + const pokemonIconsContainer = this.scene.add.container(125, 17); + let luckValue = 0; data.party.forEach((p: PokemonData, i: integer) => { const iconContainer = this.scene.add.container(26 * i, 0); @@ -290,11 +305,31 @@ class RunEntry extends Phaser.GameObjects.Container { pokemonIconsContainer.add(iconContainer); + luckValue += pokemon.getLuck(); + pokemon.destroy(); }); this.add(pokemonIconsContainer); + //Display Score - only visible for Daily Mode + //Display Luck - only visible for Endless Modes + switch (data.gameMode) { + case GameModes.DAILY: + const runScore = data.score; + const scoreText = addTextObject(this.scene, 240, 5, `Score: ${data.score}`, TextStyle.WINDOW, {color: "#f89890"}); + this.add(scoreText); + break; + case GameModes.ENDLESS: + case GameModes.SPLICED_ENDLESS: + if (luckValue > 14) { + luckValue = 14; + } + const luckTextTint = "#"+(getLuckTextTint(luckValue)).toString(16); + const luckText = addTextObject(this.scene, 240, 5, `Luck: ${getLuckString(luckValue)}`, TextStyle.WINDOW, {color: `${luckTextTint}`}); + this.add(luckText); + break; + } /* const modifiersModule = import("../modifier/modifier"); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts new file mode 100644 index 00000000000..365db67a05b --- /dev/null +++ b/src/ui/run-info-ui-handler.ts @@ -0,0 +1,659 @@ +import BattleScene from "../battle-scene"; +import { GameModes } from "../game-mode"; +import UiHandler from "./ui-handler"; +import { SessionSaveData } from "../system/game-data"; +import { TextStyle, addTextObject, addBBCodeTextObject } from "./text"; +import { Mode } from "./ui"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import PokemonData from "../system/pokemon-data"; +import i18next from "i18next"; +import {Button} from "../enums/buttons"; +import { BattleType } from "../battle"; +import { TrainerVariant } from "../field/trainer"; +import { Challenges } from "#enums/challenges"; +import { getLuckString, getLuckTextTint } from "../modifier/modifier-type"; +import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle.js"; +import { Type, getTypeRgb } from "../data/type"; +import { starterPassiveAbilities } from "../data/pokemon-species"; +import { getNatureStatMultiplier, getNatureName } from "../data/nature"; +import { allAbilities } from "../data/ability"; +import { getVariantTint } from "#app/data/variant"; +import { PokemonHeldItemModifier } from "../modifier/modifier"; +import {modifierSortFunc} from "../modifier/modifier"; +import { Species } from "#enums/species"; + +/* +enum Page { + GENERAL, + STATS, + HALL_OF_FAME +} +*/ + + +export enum RunVictory { + DEFEATED, + VICTORY +} + +export default class GameInfoUiHandler extends UiHandler { + private runInfo: SessionSaveData; + private victory: boolean; + + private gameStatsContainer: Phaser.GameObjects.Container; + private statsContainer: Phaser.GameObjects.Container; + + private runResultContainer: Phaser.GameObjects.Container; + private runInfoContainer: Phaser.GameObjects.Container; + private partyContainer: Phaser.GameObjects.Container; + private partyHeldItemsContainer: Phaser.GameObjects.Container; + private statsBgWidth: integer; + private partyContainerHeight: integer; + private partyContainerWidth: integer; + + private hallofFameContainer: Phaser.GameObjects.Container; + + private partyInfo: Phaser.GameObjects.Container[]; + private partyVisibility: Boolean; + private modifiersModule: any; + + private statValues: Phaser.GameObjects.Text[]; + + constructor(scene: BattleScene) { + super(scene, Mode.RUN_INFO); + } + + async setup() { + //const page = 0; + this.gameStatsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); + this.modifiersModule = await import("../modifier/modifier"); + this.gameStatsContainer.setVisible(false); + } + + show(args: any[]): boolean { + super.show(args); + + const gameStatsBg = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width, this.scene.game.canvas.height, 0x006860); + gameStatsBg.setOrigin(0, 0); + this.gameStatsContainer.add(gameStatsBg); + + const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6) - 2, 24); + headerBg.setOrigin(0, 0); + + const downButtonContainer = this.scene.add.container(0, 0); + const downButtonText = addTextObject(this.scene, 8, 0, i18next.t("runHistory:viewHeldItems"), TextStyle.WINDOW, {fontSize:"34px"}); + const downButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 2, "keyboard", "KEY_ARROW_DOWN.png"); + downButtonContainer.add([downButtonText, downButtonElement]); + downButtonContainer.setPositionRelative(headerBg, 275, 10); + const headerText = addTextObject(this.scene, 0, 0, i18next.t("runHistory:runInfo"), TextStyle.SETTINGS_LABEL); + headerText.setOrigin(0, 0); + headerText.setPositionRelative(headerBg, 8, 4); + this.gameStatsContainer.add(headerBg); + this.gameStatsContainer.add(downButtonContainer); + this.gameStatsContainer.add(headerText); + + const run = args[0]; + this.runInfo = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); + this.victory = run.victory; + + this.statsBgWidth = ((this.scene.game.canvas.width / 6) - 2) / 3; + + this.runResultContainer = this.scene.add.container(0, 24); + const runResultWindow = addWindow(this.scene, 0, 0, this.statsBgWidth-11, 65); + runResultWindow.setOrigin(0, 0); + this.runResultContainer.add(runResultWindow); + this.parseRunResult(this.runInfo, this.victory); + + this.partyContainer = this.scene.add.container(this.statsBgWidth-10, 23); + + this.setCursor(0); + + this.runInfoContainer = this.scene.add.container(0, 89); + const runInfoWindow = addWindow(this.scene, 0, 0, this.statsBgWidth-11, 90); + this.runInfoContainer.add(runInfoWindow); + this.parseRunInfo(this.runInfo); + + const partyData = this.runInfo.party; + this.parsePartyInfo(partyData); + this.showParty(true); + + this.gameStatsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + this.getUi().bringToTop(this.gameStatsContainer); + this.gameStatsContainer.setVisible(true); + + if (this.victory) { + this.createHallofFame(); + this.getUi().bringToTop(this.hallofFameContainer); + } + + this.setCursor(0); + + this.getUi().add(this.gameStatsContainer); + //this.updateStats(); + + this.getUi().hideTooltip(); + + return true; + } + + async parseRunResult(runData: any, runResult: boolean) { + const runResultText = addBBCodeTextObject(this.scene, 6, 4, `${(runResult ? i18next.t("runHistory:victory") : i18next.t("runHistory:defeated")+" - Wave "+runData.waveIndex)}`, TextStyle.WINDOW, {fontSize : "65px", lineSpacing: 0.1}); + this.runResultContainer.add(runResultText); + + if (runResult) { + const hallofFameInstructionContainer = this.scene.add.container(0, 0); + const upButtonText = addTextObject(this.scene, 8, 0, i18next.t("runHistory:viewHallOfFame"), TextStyle.WINDOW, {fontSize:"65px"}); + const upButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 4, "keyboard", "KEY_ARROW_UP.png"); + hallofFameInstructionContainer.add([upButtonText, upButtonElement]); + hallofFameInstructionContainer.setPosition(12, 25); + this.runResultContainer.add(hallofFameInstructionContainer); + } + + if (!runResult) { + const enemyContainer = this.scene.add.container(0, 0); + + //Wild - Single and Doubles + if (runData.battleType === BattleType.WILD) { + switch (runData.enemyParty.length) { + case 1: + //Wild - Singles + const enemyIconContainer = this.scene.add.container(0, 0); + const enemyData = runData.enemyParty[0]; + const bossStatus = enemyData.boss; + enemyData.boss = false; + //addPokemonIcon() throws an error if the Pokemon used is a boss + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemyLevel = addTextObject(this.scene, 36, 26, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, bossStatus ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "44px", color: "#f8f8f8" }); + enemyLevel.setShadow(0, 0, null); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(1, 0); + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyContainer.add(enemyIconContainer); + enemyContainer.setPosition(27, 10); + enemy.destroy(); + break; + case 2: + runData.enemyParty.forEach((enemyData, e) => { + const enemyIconContainer = this.scene.add.container(0, 0); + const bossStatus = enemyData.boss; + enemyData.boss = false; + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemyLevel = addTextObject(this.scene, 36, 26, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, bossStatus ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "44px", color: "#f8f8f8" }); + enemyLevel.setShadow(0, 0, null); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(1, 0); + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyIconContainer.setPosition(e*35, 0); + enemyContainer.add(enemyIconContainer); + enemy.destroy(); + }); + enemyContainer.setPosition(8, 14); + break; + } + //Trainer - Single and Double + } else if (runData.battleType === BattleType.TRAINER) { + const tObj = runData.trainer.toTrainer(this.scene); + const tObjSpriteKey = tObj.config.getSpriteKey(runData.trainer.variant === TrainerVariant.FEMALE, false); + const tObjSprite = this.scene.add.sprite(0, 0, tObjSpriteKey); + if (runData.trainer.variant === TrainerVariant.DOUBLE) { + const doubleContainer = this.scene.add.container(5, 8); + tObjSprite.setPosition(-3, -3); + const tObjPartnerSpriteKey = tObj.config.getSpriteKey(true, true); + const tObjPartnerSprite = this.scene.add.sprite(5, -3, tObjPartnerSpriteKey); + tObjPartnerSprite.setScale(0.20); + tObjSprite.setScale(0.20); + doubleContainer.add(tObjSprite); + doubleContainer.add(tObjPartnerSprite); + enemyContainer.add(doubleContainer); + } else { + tObjSprite.setScale(0.25, 0.25); + tObjSprite.setPosition(9, 23); + enemyContainer.add(tObjSprite); + } + + const teraPokemon = {}; + runData.enemyModifiers.forEach((m) => { + if (m.className === "TerastallizeModifier") { + teraPokemon[m.args[0]] = m.args[1]; + } + }); + + const enemyPartyContainer = this.scene.add.container(0, 0); + runData.enemyParty.forEach((enemyData, e) => { + const pokemonRowHeight = Math.floor(e/3); + const enemyIconContainer = this.scene.add.container(0, 0); + enemyIconContainer.setScale(0.6); + const isBoss = enemyData.boss; + enemyData.boss = false; + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemySprite1 = enemyIcon.list[0] as Phaser.GameObjects.Sprite; + const enemySprite2 = (enemyIcon.list.length > 1) ? enemyIcon.list[1] as Phaser.GameObjects.Sprite : null; + if (teraPokemon[enemyData.id]) { + const teraTint = getTypeRgb(teraPokemon[enemyData.id]); + const teraColor = new Phaser.Display.Color(teraTint[0], teraTint[1], teraTint[2]); + enemySprite1.setTint(teraColor.color); + if (enemySprite2) { + enemySprite2.setTint(teraColor.color); + } + } + enemyIcon.setPosition(39*(e%3), (35*pokemonRowHeight)); + const enemyLevel = addTextObject(this.scene, 43*(e%3), (27*(pokemonRowHeight+1)), `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, isBoss ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "54px" }); + enemyLevel.setShadow(0, 0, null); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(0, 0); + + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyPartyContainer.add(enemyIconContainer); + enemy.destroy(); + }); + enemyPartyContainer.setPosition(25, 18); + enemyContainer.add(enemyPartyContainer); + } + this.runResultContainer.add(enemyContainer); + } + this.gameStatsContainer.add(this.runResultContainer); + } + + async parseRunInfo(runData:any) { + const modeText = addBBCodeTextObject(this.scene, 7, 0, "", TextStyle.WINDOW, {fontSize : "50px", lineSpacing:3}); + modeText.setPosition(7, 5); + modeText.appendText(i18next.t("runHistory:mode")+": ", false); + switch (runData.gameMode) { + case GameModes.DAILY: + modeText.appendText(`${i18next.t("gameMode:dailyRun")}`, false); + break; + case GameModes.SPLICED_ENDLESS: + modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); + if (runData.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { + modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false); + modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + break; + case GameModes.CHALLENGE: + modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); + modeText.appendText(`\t\t${i18next.t("runHistory:challengeRules")}: `); + const runChallenges = runData.challenges; + const rules = []; + for (let i = 0; i < runChallenges.length; i++) { + if (runChallenges[i].id === Challenges.SINGLE_GENERATION && runChallenges[i].value !== 0) { + rules.push(i18next.t(`runHistory:challengeMonoGen${runChallenges[i].value}` as const)); + } else if (runChallenges[i].id === Challenges.SINGLE_TYPE && runChallenges[i].value !== 0) { + rules.push(i18next.t(`pokemonInfo:Type.${Type[runChallenges[i].value-1]}` as const)); + } + } + if (rules) { + for (let i = 0; i < rules.length; i++) { + if (i > 0) { + modeText.appendText(" + ", false); + } + modeText.appendText(rules[i], false); + } + } + break; + case GameModes.ENDLESS: + modeText.appendText(`${i18next.t("gameMode:endless")}`, false); + if (runData.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { + modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false); + modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + break; + case GameModes.CLASSIC: + modeText.appendText(`${i18next.t("gameMode:classic")}`, false); + break; + } + + const runInfoTextContainer = this.scene.add.container(0, 0); + const runInfoText = addBBCodeTextObject(this.scene, 7, 0, "", TextStyle.WINDOW, {fontSize : "50px", lineSpacing:3}); + const runTime = Utils.getPlayTimeString(runData.playTime); + runInfoText.appendText(`${i18next.t("runHistory:runLength")}: ${runTime}`, false); + runInfoText.appendText(`[color=#e8e8a8]\u20BD${Utils.formatLargeNumber(runData.money, 1000)}[/color]`); + const luckText = addBBCodeTextObject(this.scene, 0, 0, "", TextStyle.WINDOW, {fontSize: "55px"}); + const luckValue = Phaser.Math.Clamp(runData.party.map(p => p.toPokemon(this.scene).getLuck()).reduce((total: integer, value: integer) => total += value, 0), 0, 14); + let luckInfo = i18next.t("runHistory:luck")+": "+getLuckString(luckValue); + if (luckValue < 14) { + luckInfo = "[color=#"+(getLuckTextTint(luckValue)).toString(16)+"]"+luckInfo+"[/color]"; + } else { + luckText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + luckText.appendText("[align=right]"+luckInfo+"[/align]", false); + runInfoText.setPosition(7, 70); + luckText.setPosition(62,77); + runInfoTextContainer.add(runInfoText); + runInfoTextContainer.add(luckText); + + + if (runData.modifiers.length) { + let visibleModifierIndex = 0; + + const modifierIconsContainer = this.scene.add.container(8, (runData.gameMode === GameModes.CHALLENGE) ? 20 : 15); + modifierIconsContainer.setScale(0.45); + for (const m of runData.modifiers) { + const modifier = m.toModifier(this.scene, this.modifiersModule[m.className]); + if (modifier instanceof PokemonHeldItemModifier) { + continue; + } + const item = this.scene.add.sprite(0, 12, "items"); + item.setFrame(modifier.type.iconImage); + + item.setOrigin(0, 0.5); + const rowHeightModifier = Math.floor(visibleModifierIndex/7); + item.setPosition(24 * (visibleModifierIndex%7), 20+(35*rowHeightModifier)); + + modifierIconsContainer.add(item); + const maxStackCount = modifier.getMaxStackCount(this.scene); + if (maxStackCount > 1) { + const itemStackCount = addTextObject(this.scene, (24*(visibleModifierIndex%7))+22, 22+(35*rowHeightModifier), modifier.stackCount, TextStyle.PARTY, {fontSize:"64px"}); + if (modifier.stackCount === maxStackCount) { + itemStackCount.setColor("#f89890"); + } + modifierIconsContainer.add(itemStackCount); + } + + if (++visibleModifierIndex === 20) { + const maxItems = addTextObject(this.scene, 45, 90, "+", TextStyle.WINDOW); + maxItems.setPositionRelative(modifierIconsContainer, 70, 45); + this.runInfoContainer.add(maxItems); + break; + } + } + this.runInfoContainer.add(modifierIconsContainer); + } + + this.runInfoContainer.add(modeText); + this.runInfoContainer.add(runInfoTextContainer); + this.gameStatsContainer.add(this.runInfoContainer); + } + + parsePartyInfo(party: any): void { + + const currentLanguage = i18next.resolvedLanguage; + const windowHeight = ((this.scene.game.canvas.height / 6) - 23)/6; + + party.forEach((p: PokemonData, i: integer) => { + const pokemonInfoWindow = new RoundRectangle(this.scene, 0, 14, (this.statsBgWidth*2)+10, windowHeight-2, 3); + + const pokemon = p.toPokemon(this.scene); + const pokemonInfoContainer = this.scene.add.container(this.statsBgWidth+5, (windowHeight-0.5)*i); + + const types = pokemon.getTypes(); + let typeColor = getTypeRgb(types[0]); + const type1Color = new Phaser.Display.Color(typeColor[0], typeColor[1], typeColor[2]); + + const bgColor = type1Color.clone().darken(45); + pokemonInfoWindow.setFillStyle(bgColor.color); + + const iconContainer = this.scene.add.container(0, 0); + const icon = this.scene.addPokemonIcon(pokemon, 0, 0, 0, 0); + icon.setScale(0.75); + icon.setPosition(-99, 1); + typeColor = types[1] ? getTypeRgb(types[1]) : null; + const type2Color = typeColor ? new Phaser.Display.Color(typeColor[0], typeColor[1], typeColor[2]) : null; + type2Color ? pokemonInfoWindow.setStrokeStyle(1, type2Color.color, 0.95) : pokemonInfoWindow.setStrokeStyle(1, type1Color.color, 0.95); + + this.getUi().bringToTop(icon); + + const pokeInfoTextContainer = this.scene.add.container(-85, 3.5); + const textContainerFontSize = "34px"; + const pSpecies = pokemon.species; + const pNature = getNatureName(pokemon.nature); + const pName = pokemon.fusionSpecies ? pokemon.name : pSpecies.name; + const passiveLabel = (currentLanguage==="ko"||currentLanguage==="zh_CN"||currentLanguage==="zh_TW") ? i18next.t("starterSelectUiHandler:passive") : i18next.t("starterSelectUiHandler:passive").charAt(0); + const abilityLabel = (currentLanguage==="ko"||currentLanguage==="zh_CN"||currentLanguage==="zh_TW") ? i18next.t("starterSelectUiHandler:ability") : i18next.t("starterSelectUiHandler:ability").charAt(0); + const pPassiveInfo = pokemon.passive ? `${passiveLabel+": "+allAbilities[starterPassiveAbilities[pSpecies.speciesId]].name}` : ""; + const pAbilityInfo = abilityLabel + ": " + pokemon.getAbility().name; + const pokeInfoText = addBBCodeTextObject(this.scene, 0, 0, pName, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); + pokeInfoText.appendText(`${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatFancyLargeNumber(pokemon.level, 1)} - ${pNature}`); + pokeInfoText.appendText(pAbilityInfo); + pokeInfoText.appendText(pPassiveInfo); + pokeInfoTextContainer.add(pokeInfoText); + + const pokeStatTextContainer = this.scene.add.container(-35, 6); + const pStats = []; + pokemon.stats.forEach((element) => pStats.push(Utils.formatFancyLargeNumber(element,1))); + + for (let i = 0; i < pStats.length; i++) { + const isMult = getNatureStatMultiplier(pokemon.nature, i); + pStats[i] = (isMult < 1) ? pStats[i] + "[color=#40c8f8]↓[/color]" : pStats[i]; + pStats[i] = (isMult > 1) ? pStats[i] + "[color=#f89890]↑[/color]" : pStats[i]; + } + + const hp = i18next.t("pokemonInfo:Stat.HPshortened")+": "+pStats[0]; + const atk = i18next.t("pokemonInfo:Stat.ATKshortened")+": "+pStats[1]; + const def = i18next.t("pokemonInfo:Stat.DEFshortened")+": "+pStats[2]; + const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened")+": "+pStats[3]; + const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened")+": "+pStats[4]; + const speedLabel = (currentLanguage==="es"||currentLanguage==="pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); + const speed = speedLabel+": "+pStats[5]; + + //Column 1: HP Atk Def + const pokeStatText1 = addBBCodeTextObject(this.scene, -5, 0, hp, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); + pokeStatText1.appendText(atk); + pokeStatText1.appendText(def); + pokeStatTextContainer.add(pokeStatText1); + //Column 2: SpAtk SpDef Speed + const pokeStatText2 = addBBCodeTextObject(this.scene, 25, 0, spatk, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); + pokeStatText2.appendText(spdef); + pokeStatText2.appendText(speed); + pokeStatTextContainer.add(pokeStatText2); + + const marksContainer = this.scene.add.container(0, 0); + + if (pokemon.fusionSpecies) { + const splicedIcon = this.scene.add.image(0, 0, "icon_spliced"); + splicedIcon.setScale(0.35); + splicedIcon.setOrigin(0, 0); + pokemon.shiny ? splicedIcon.setPositionRelative(pokeInfoTextContainer, 35, 0) : splicedIcon.setPositionRelative(pokeInfoTextContainer, 28, 0); + marksContainer.add(splicedIcon); + this.getUi().bringToTop(splicedIcon); + } + + if (pokemon.isShiny()) { + const doubleShiny = pokemon.isFusion() && pokemon.shiny && pokemon.fusionShiny; + + const shinyStar = this.scene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`); + shinyStar.setOrigin(0, 0); + shinyStar.setScale(0.65); + shinyStar.setPositionRelative(pokeInfoTextContainer, 28, 0); + shinyStar.setTint(getVariantTint(!doubleShiny ? pokemon.getVariant() : pokemon.variant)); + marksContainer.add(shinyStar); + this.getUi().bringToTop(shinyStar); + + if (doubleShiny) { + const fusionShinyStar = this.scene.add.image(0, 0, "shiny_star_small_2"); + fusionShinyStar.setOrigin(0, 0); + fusionShinyStar.setScale(0.5); + fusionShinyStar.setPosition(shinyStar.x, shinyStar.y); + fusionShinyStar.setTint(getVariantTint(pokemon.fusionVariant)); + marksContainer.add(fusionShinyStar); + this.getUi().bringToTop(fusionShinyStar); + } + } + + const pokemonMoveset = pokemon.getMoveset(); + const movesetContainer = this.scene.add.container(70, -29); + const pokemonMoveBgs = []; + const pokemonMoveLabels = []; + const movePos = [[-6.5,35.5],[37,35.5],[-6.5,43.5],[37,43.5]]; + for (let m = 0; m < pokemonMoveset.length; m++) { + const moveContainer = this.scene.add.container(movePos[m][0], movePos[m][1]); + moveContainer.setScale(0.5); + + const moveBg = this.scene.add.nineslice(0, 0, "type_bgs", "unknown", 85, 15, 2, 2, 2, 2); + moveBg.setOrigin(1, 0); + + const moveLabel = addTextObject(this.scene, -moveBg.width / 2, 2, "-", TextStyle.PARTY); + moveLabel.setOrigin(0.5, 0); + moveLabel.setName("text-move-label"); + + pokemonMoveBgs.push(moveBg); + pokemonMoveLabels.push(moveLabel); + + moveContainer.add(moveBg); + moveContainer.add(moveLabel); + + movesetContainer.add(moveContainer); + + const move = m < pokemonMoveset.length ? pokemonMoveset[m].getMove() : null; + pokemonMoveBgs[m].setFrame(Type[move ? move.type : Type.UNKNOWN].toString().toLowerCase()); + pokemonMoveLabels[m].setText(move ? move.name : "-"); + } + + const heldItemsScale = (this.runInfo.gameMode === GameModes.SPLICED_ENDLESS || this.runInfo.gameMode === GameModes.ENDLESS) ? 0.25 : 0.5; + const heldItemsContainer = this.scene.add.container(-82, 6); + const heldItemsList = []; + if (this.runInfo.modifiers.length) { + for (const m of this.runInfo.modifiers) { + const modifier = m.toModifier(this.scene, this.modifiersModule[m.className]); + if (modifier instanceof PokemonHeldItemModifier && modifier.pokemonId === pokemon.id) { + modifier.stackCount = m["stackCount"]; + heldItemsList.push(modifier); + } + } + if (heldItemsList.length > 0) { + (heldItemsList as PokemonHeldItemModifier[]).sort(modifierSortFunc); + let row = 0; + for (const [index, item] of heldItemsList.entries()) { + if ( index > 36 ) { + const overflowIcon = addTextObject(this.scene, 182, 4, "+", TextStyle.WINDOW); + heldItemsContainer.add(overflowIcon); + break; + } + const itemIcon = item.getIcon(this.scene, true); + //itemIcon.setFrame(item.type.iconImage); + itemIcon.setScale(heldItemsScale); + itemIcon.setPosition((index%19)*10, row*10); + heldItemsContainer.add(itemIcon); + if (index !== 0 && index%18 === 0) { + row++; + } + } + } + } + heldItemsContainer.setName("heldItems"); + heldItemsContainer.setVisible(false); + + pokemonInfoContainer.add(pokemonInfoWindow); + iconContainer.add(icon); + pokemonInfoContainer.add(iconContainer); + marksContainer.setName("PkmnMarks"); + pokemonInfoContainer.add(marksContainer); + movesetContainer.setName("PkmnMoves"); + pokemonInfoContainer.add(movesetContainer); + pokeInfoTextContainer.setName("PkmnInfoText"); + pokemonInfoContainer.add(pokeInfoTextContainer); + pokeStatTextContainer.setName("PkmnStatsText"); + pokemonInfoContainer.add(pokeStatTextContainer); + pokemonInfoContainer.add(heldItemsContainer); + pokemonInfoContainer.setName("PkmnInfo"); + this.partyContainer.add(pokemonInfoContainer); + pokemon.destroy(); + }); + this.gameStatsContainer.add(this.partyContainer); + } + + showParty(partyVisible: boolean): void { + const allContainers = this.partyContainer.getAll("name", "PkmnInfo"); + allContainers.forEach((c: Phaser.GameObjects.Container) => { + console.log(c.getByName("PkmnMoves")); + c.getByName("PkmnMoves").setVisible(partyVisible); + c.getByName("PkmnInfoText").setVisible(partyVisible); + c.getByName("PkmnStatsText").setVisible(partyVisible); + c.getByName("PkmnMarks").setVisible(partyVisible); + c.getByName("heldItems").setVisible(!partyVisible); + this.partyVisibility = partyVisible; + }); + } + + createHallofFame(): void { + this.hallofFameContainer = this.scene.add.container(0, 0); + //Thank you Hayuna for the code + const hallofFameBg = this.scene.add.image(0, 0, "hall_of_fame"); + hallofFameBg.setPosition(159, 89); + hallofFameBg.setSize(this.scene.game.canvas.width, this.scene.game.canvas.height+10); + this.hallofFameContainer.add(hallofFameBg); + const hallofFameText = addTextObject(this.scene, 0, 0, i18next.t("runHistory:hallofFameText"), TextStyle.WINDOW); + hallofFameText.setPosition(85, 145); + this.hallofFameContainer.add(hallofFameText); + this.runInfo.party.forEach((p, i) => { + const species = p.toPokemon(this.scene); + const row = i % 2; + const id = species.id; + const shiny = species.shiny; + const formIndex = species.formIndex; + const variant = species.variant; + const pokemonSprite: Phaser.GameObjects.Sprite = this.scene.add.sprite(60 + 40 * i, 40 + row * 60, "pkmn__sub"); + pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + this.hallofFameContainer.add(pokemonSprite); + const speciesLoaded: Map = new Map(); + speciesLoaded.set(id, false); + + const female = species.gender === 1; + species.species.loadAssets(this.scene, female, formIndex, shiny, variant, true).then(() => { + speciesLoaded.set(id, true); + pokemonSprite.play(species.species.getSpriteKey(female, formIndex, shiny, variant)); + pokemonSprite.setPipelineData("shiny", shiny); + pokemonSprite.setPipelineData("variant", variant); + pokemonSprite.setPipelineData("spriteKey", species.species.getSpriteKey(female, formIndex, shiny, variant)); + pokemonSprite.setVisible(true); + }); + species.destroy(); + }); + this.hallofFameContainer.setVisible(false); + this.gameStatsContainer.add(this.hallofFameContainer); + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + const error = false; + + if (button === Button.CANCEL) { + success = true; + this.runInfoContainer.removeAll(true); + this.runResultContainer.removeAll(true); + this.partyContainer.removeAll(true); + this.gameStatsContainer.removeAll(true); + this.hallofFameContainer.removeAll(true); + super.clear(); + this.gameStatsContainer.setVisible(false); + ui.revertMode(); + } else { + switch (button) { + case Button.DOWN: + if (this.partyVisibility) { + this.showParty(false); + } else { + this.showParty(true); + } + break; + case Button.UP: + if (this.victory) { + if (!this.hallofFameContainer.visible) { + this.hallofFameContainer.setVisible(true); + break; + } else { + this.hallofFameContainer.setVisible(false); + break; + } + break; + } + break; + } + } + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + return success || error; + } +} + From e4113a929a20d2c5f3b4b879aa9cfd0b9f3f5b09 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Sun, 16 Jun 2024 12:35:02 -0700 Subject: [PATCH 03/35] Updated code with the current repo + mode localization --- src/system/game-data.ts | 49 +++++++++++++++----------------- src/ui/run-history-ui-handler.ts | 24 ++++++++++++---- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index d7c796ab339..a6b71c5ed0b 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -945,31 +945,26 @@ export class GameData { } async public getRunHistoryData(scene: BattleScene): Promise { - try { + if (!Utils.isLocal) { const response = await Utils.apiFetch("savedata/runHistory", true); const data = await response.json(); - console.log(data); - if (!data) { - throw new Error("No data"); - } else { - var 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; - } - return data; - } - } catch (err) { - console.log("Something went wrong: ", err); - var cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + const cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); if (cachedResponse) { cachedResponse = JSON.parse(decrypt(cachedResponse, true)); } - return cachedResponse ?? {}; + 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; + } + return data; + } else { + const cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + if (cachedResponse) { + cachedResponse = JSON.parse(decrypt(cachedResponse, true)); + } + const cachedRHData = cachedResponse ?? {}; + return cachedRHData; } } @@ -990,12 +985,14 @@ export class GameData { localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt(JSON.stringify(runHistoryData), true)); - try { - const response = Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); - return true; - } catch (err) { - console.log("savedata/runHistory POST failed : ", err); - return false; + if (!Utils.local) { + try { + const response = Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); + return true; + } catch (err) { + console.log("savedata/runHistory POST failed : ", err); + return false; + } } } diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 05f53c9bf1c..9f773c97c7b 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -1,5 +1,5 @@ import BattleScene from "../battle-scene"; -import { gameModes, GameModes } from "../game-mode"; +import { GameModes } from "../game-mode"; import { SessionSaveData, parseSessionData, getRunHistoryData, RunHistoryData, RunEntries, decrypt } from "../system/game-data"; import { TextStyle, addTextObject } from "./text"; import { Mode } from "./ui"; @@ -13,7 +13,7 @@ import MessageUiHandler from "./message-ui-handler"; import i18next from "i18next"; import {Button} from "../enums/buttons"; import { BattleType } from "../battle"; -import {TrainerType} from "../data/enums/trainer-type"; +import { TrainerType } from "../enums/trainer-type"; import { TrainerVariant } from "../field/trainer"; import { getPartyLuckValue, getLuckString, getLuckTextTint } from "../modifier/modifier-type"; @@ -134,7 +134,6 @@ export default class RunHistoryUiHandler extends MessageUiHandler { async populateruns(scene: BattleScene) { const response = await this.scene.gameData.getRunHistoryData(this.scene); - console.log(response); const timestamps = Object.keys(response); if (timestamps.length > 1) { timestamps.sort((a, b) => a - b); @@ -278,9 +277,24 @@ class RunEntry extends Phaser.GameObjects.Container { } } - const gameModeLabel = addTextObject(this.scene, 8, 19, `${gameModes[data.gameMode]?.getName() || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); - this.add(gameModeLabel); + switch (data.gameMode) { + case GameModes.DAILY: + const dailyModeLabel = addTextObject(this.scene, 8, 19, `${i18next.t('gameMode:dailyRun') || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); + this.add(dailyModeLabel); + break; + case GameModes.SPLICED_ENDLESS: + const endlessSplicedLabel = addTextObject(this.scene, 8, 19, `${i18next.t('gameMode:endlessSpliced') || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); + this.add(endlessSplicedLabel); + break; + case GameModes.ENDLESS: + case GameModes.CLASSIC: + case GameModes.CHALLENGE: + const gameModeLabel = addTextObject(this.scene, 8, 19, `${i18next.t('gameMode:'+GameModes[data.gameMode].toLowerCase()) || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); + this.add(gameModeLabel); + break; + } + const date = new Date(data.timestamp); const timestampLabel = addTextObject(this.scene, 8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); From bc125f145b393df00f09bb513f885412ca2974c8 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Mon, 17 Jun 2024 09:37:24 -0700 Subject: [PATCH 04/35] Final touches before moving to a clean branch --- src/loading-scene.ts | 3 + src/locales/de/run-history-ui-handler.ts | 31 +++++ src/locales/en/config.ts | 35 +----- src/locales/en/run-history-ui-handler.ts | 35 ++++++ src/locales/es/run-history-ui-handler.ts | 30 +++++ src/locales/fr/run-history-ui-handler.ts | 31 +++++ src/locales/it/run-history-ui-handler.ts | 31 +++++ src/locales/ko/run-history-ui-handler.ts | 31 +++++ src/locales/pt_BR/run-history-ui-handler.ts | 31 +++++ src/locales/zh_CN/run-history-ui-handler.ts | 31 +++++ src/locales/zh_TW/run-history-ui-handler.ts | 31 +++++ src/system/game-data.ts | 38 +++--- src/ui/run-history-ui-handler.ts | 130 +++++++------------- src/ui/ui.ts | 33 +++-- 14 files changed, 371 insertions(+), 150 deletions(-) create mode 100644 src/locales/de/run-history-ui-handler.ts create mode 100644 src/locales/en/run-history-ui-handler.ts create mode 100644 src/locales/es/run-history-ui-handler.ts create mode 100644 src/locales/fr/run-history-ui-handler.ts create mode 100644 src/locales/it/run-history-ui-handler.ts create mode 100644 src/locales/ko/run-history-ui-handler.ts create mode 100644 src/locales/pt_BR/run-history-ui-handler.ts create mode 100644 src/locales/zh_CN/run-history-ui-handler.ts create mode 100644 src/locales/zh_TW/run-history-ui-handler.ts diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 5275411055e..236c67af7c2 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -215,6 +215,9 @@ export class LoadingScene extends SceneBase { this.loadAtlas("c_rival_m", "character", "rival_m"); this.loadAtlas("c_rival_f", "character", "rival_f"); + //Load run-history related images + this.loadImage("hall_of_fame", "ui"); + // Load pokemon-related images this.loadImage("pkmn__back__sub", "pokemon/back", "sub.png"); this.loadImage("pkmn__sub", "pokemon", "sub.png"); diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/de/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index ee180163eb1..072a6a5bb73 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -1,16 +1,9 @@ -import { common } from "./common.js"; -import { settings } from "./settings.js"; import { ability } from "./ability"; import { abilityTriggers } from "./ability-trigger"; -import { arenaFlyout } from "./arena-flyout"; -import { arenaTag } from "./arena-tag"; import { PGFachv, PGMachv } from "./achv"; import { battle } from "./battle"; -import { battleInfo } from "./battle-info"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; -import { battlerTags } from "./battler-tags"; import { berry } from "./berry"; -import { bgmName } from "./bgm-name"; import { biome } from "./biome"; import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; @@ -26,49 +19,37 @@ import { } from "./dialogue"; import { egg } from "./egg"; import { fightUiHandler } from "./fight-ui-handler"; -import { filterBar } from "./filter-bar"; import { gameMode } from "./game-mode"; import { gameStatsUiHandler } from "./game-stats-ui-handler"; import { growth } from "./growth"; import { menu } from "./menu"; import { menuUiHandler } from "./menu-ui-handler"; -import { modifier } from "./modifier"; import { modifierType } from "./modifier-type"; import { move } from "./move"; import { nature } from "./nature"; import { partyUiHandler } from "./party-ui-handler"; import { pokeball } from "./pokeball"; import { pokemon } from "./pokemon"; -import { pokemonForm } from "./pokemon-form"; import { pokemonInfo } from "./pokemon-info"; import { pokemonInfoContainer } from "./pokemon-info-container"; -import { pokemonSummary } from "./pokemon-summary"; import { saveSlotSelectUiHandler } from "./save-slot-select-ui-handler"; import { splashMessages } from "./splash-messages"; import { starterSelectUiHandler } from "./starter-select-ui-handler"; -import { statusEffect } from "./status-effect"; import { titles, trainerClasses, trainerNames } from "./trainers"; import { tutorial } from "./tutorial"; import { voucher } from "./voucher"; -import { terrain, weather } from "./weather"; -import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; -import { moveTriggers } from "./move-trigger"; +import { weather } from "./weather"; +import { runHistory } from "./run-history-ui-handler.ts"; export const enConfig = { ability: ability, abilityTriggers: abilityTriggers, - arenaFlyout: arenaFlyout, - arenaTag: arenaTag, battle: battle, - battleInfo: battleInfo, battleMessageUiHandler: battleMessageUiHandler, - battlerTags: battlerTags, berry: berry, - bgmName: bgmName, biome: biome, challenges: challenges, commandUiHandler: commandUiHandler, - common: common, PGMachv: PGMachv, PGFachv: PGFachv, PGMdialogue: PGMdialogue, @@ -81,35 +62,27 @@ export const enConfig = { PGFdoubleBattleDialogue: PGFdoubleBattleDialogue, egg: egg, fightUiHandler: fightUiHandler, - filterBar: filterBar, gameMode: gameMode, gameStatsUiHandler: gameStatsUiHandler, growth: growth, menu: menu, menuUiHandler: menuUiHandler, - modifier: modifier, modifierType: modifierType, move: move, nature: nature, + partyUiHandler: partyUiHandler, pokeball: pokeball, pokemon: pokemon, - pokemonForm: pokemonForm, pokemonInfo: pokemonInfo, pokemonInfoContainer: pokemonInfoContainer, - pokemonSummary: pokemonSummary, saveSlotSelectUiHandler: saveSlotSelectUiHandler, - settings: settings, splashMessages: splashMessages, starterSelectUiHandler: starterSelectUiHandler, - statusEffect: statusEffect, - terrain: terrain, titles: titles, trainerClasses: trainerClasses, trainerNames: trainerNames, tutorial: tutorial, voucher: voucher, weather: weather, - partyUiHandler: partyUiHandler, - modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + runHistory: runHistory, }; diff --git a/src/locales/en/run-history-ui-handler.ts b/src/locales/en/run-history-ui-handler.ts new file mode 100644 index 00000000000..76b0102e5ef --- /dev/null +++ b/src/locales/en/run-history-ui-handler.ts @@ -0,0 +1,35 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/es/run-history-ui-handler.ts b/src/locales/es/run-history-ui-handler.ts new file mode 100644 index 00000000000..5ba1f69386b --- /dev/null +++ b/src/locales/es/run-history-ui-handler.ts @@ -0,0 +1,30 @@ +import { SimpleTranslationEntries } from "#app/plugins/i18n"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/fr/run-history-ui-handler.ts b/src/locales/fr/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/fr/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/it/run-history-ui-handler.ts b/src/locales/it/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/it/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/ko/run-history-ui-handler.ts b/src/locales/ko/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/ko/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/pt_BR/run-history-ui-handler.ts b/src/locales/pt_BR/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/pt_BR/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/zh_CN/run-history-ui-handler.ts b/src/locales/zh_CN/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/zh_CN/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/zh_TW/run-history-ui-handler.ts b/src/locales/zh_TW/run-history-ui-handler.ts new file mode 100644 index 00000000000..d0f622599e2 --- /dev/null +++ b/src/locales/zh_TW/run-history-ui-handler.ts @@ -0,0 +1,31 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWild": "Defeated by ", + "defeatedTrainer": "Defeated by ", + "defeatedTrainerDouble": "Defeated by Duo", + "defeatedRival": "Defeated by Rival", + "defeated":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", +} as const; + +//Mode Information found in game-mode.ts +//Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a6b71c5ed0b..543fc5bffa6 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -944,22 +944,27 @@ export class GameData { }) as SessionSaveData; } - async public getRunHistoryData(scene: BattleScene): Promise { + public async getRunHistoryData(scene: BattleScene): Promise { if (!Utils.isLocal) { - const response = await Utils.apiFetch("savedata/runHistory", true); - const data = await response.json(); - const 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; + 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 { - const cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); if (cachedResponse) { cachedResponse = JSON.parse(decrypt(cachedResponse, true)); } @@ -970,7 +975,10 @@ export class GameData { async saveRunHistory(scene: BattleScene, runEntry : SessionSaveData, victory: boolean): Promise { - const runHistoryData = await this.getRunHistoryData(scene); + 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 @@ -985,9 +993,9 @@ export class GameData { localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt(JSON.stringify(runHistoryData), true)); - if (!Utils.local) { + if (!Utils.isLocal) { try { - const response = Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); + Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); return true; } catch (err) { console.log("savedata/runHistory POST failed : ", err); diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 9f773c97c7b..9827cd057ac 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -1,21 +1,17 @@ import BattleScene from "../battle-scene"; import { GameModes } from "../game-mode"; -import { SessionSaveData, parseSessionData, getRunHistoryData, RunHistoryData, RunEntries, decrypt } from "../system/game-data"; import { TextStyle, addTextObject } from "./text"; import { Mode } from "./ui"; import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; -import { PokemonData } from "../system/pokemon-data"; -import { TrainerData } from "../system/trainer-data"; -import Pokemon, { EnemyPokemon, PlayerPokemon } from "../field/pokemon"; -import { PokemonHeldItemModifier } from "../modifier/modifier"; +import PokemonData from "../system/pokemon-data"; import MessageUiHandler from "./message-ui-handler"; import i18next from "i18next"; import {Button} from "../enums/buttons"; import { BattleType } from "../battle"; -import { TrainerType } from "../enums/trainer-type"; import { TrainerVariant } from "../field/trainer"; -import { getPartyLuckValue, getLuckString, getLuckTextTint } from "../modifier/modifier-type"; +import { RunHistoryData } from "../system/game-data"; + export const runCount = 25; @@ -27,9 +23,8 @@ export default class RunHistoryUiHandler extends MessageUiHandler { private runsContainer: Phaser.GameObjects.Container; private runSelectMessageBox: Phaser.GameObjects.NineSlice; private runSelectMessageBoxContainer: Phaser.GameObjects.Container; - private runs: runEntry[]; + private runs: RunEntry[]; - private uiMode: RunHistoryUiMode; private runSelectCallback: RunSelectCallback; private scrollCursor: integer = 0; @@ -93,10 +88,11 @@ export default class RunHistoryUiHandler extends MessageUiHandler { const error = false; if (button === Button.ACTION || button === Button.CANCEL) { - const originalCallback = this.runSelectCallback; if (button === Button.ACTION) { const cursor = this.cursor + this.scrollCursor; - console.log("Action --> page with more detailed run information"); + if (this.runs[cursor].hasData) { + this.scene.ui.setOverlayMode(Mode.RUN_INFO, this.runs[cursor].entryData, true); + } success = true; return success; } else { @@ -135,12 +131,13 @@ export default class RunHistoryUiHandler extends MessageUiHandler { async populateruns(scene: BattleScene) { const response = await this.scene.gameData.getRunHistoryData(this.scene); const timestamps = Object.keys(response); + const timestampsNo = timestamps.map(Number); if (timestamps.length > 1) { - timestamps.sort((a, b) => a - b); + timestampsNo.sort((a, b) => a - b); } const entryCount = timestamps.length; for (let s = 0; s < entryCount; s++) { - const entry = new RunEntry(this.scene, response, timestamps[s], s); + const entry = new RunEntry(this.scene, response, timestampsNo[s].toString(), s); this.scene.add.existing(entry); this.runsContainer.add(entry); this.runs.push(entry); @@ -215,18 +212,21 @@ export default class RunHistoryUiHandler extends MessageUiHandler { class RunEntry extends Phaser.GameObjects.Container { public slotId: integer; public hasData: boolean; + public entryData: RunHistoryData; private loadingLabel: Phaser.GameObjects.Text; - constructor(scene: BattleScene, runHistory: RunHistoryData, timestamp: string, slotId: integer) { + constructor(scene: BattleScene, runHistory: any, timestamp: string, slotId: integer) { super(scene, 0, slotId*56); this.slotId = slotId; + this.hasData = true; + this.entryData = runHistory[timestamp]; - this.setup(runHistory[timestamp]); + this.setup(this.entryData); } - setup(run: RunHistoryData) { + setup(run: any) { const victory = run.victory; const data = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); @@ -236,12 +236,12 @@ class RunEntry extends Phaser.GameObjects.Container { if (victory) { - const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Victory", TextStyle.WINDOW); + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:victory")}`, TextStyle.WINDOW); this.add(gameOutcomeLabel); } else { if (data.battleType === BattleType.WILD) { const enemyContainer = this.scene.add.container(8, 5); - const gameOutcomeLabel = addTextObject(this.scene, 0, 0, "Defeated by ", TextStyle.WINDOW); + const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild")}`, TextStyle.WINDOW); enemyContainer.add(gameOutcomeLabel); data.enemyParty.forEach((enemyData, e) => { //This allows the enemyParty to be shown - doubles or sings -> 58+(e*8) @@ -250,7 +250,7 @@ class RunEntry extends Phaser.GameObjects.Container { enemyData.boss = false; const enemy = enemyData.toPokemon(this.scene); const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); - const enemyLevel = addTextObject(this.scene, 32, 20, `Lv${Utils.formatLargeNumber(enemy.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); + const enemyLevel = addTextObject(this.scene, 32, 20, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); enemyLevel.setShadow(0, 0, null); enemyLevel.setStroke("#424242", 14); enemyLevel.setOrigin(1, 0); @@ -262,46 +262,46 @@ class RunEntry extends Phaser.GameObjects.Container { this.add(enemyContainer); } else if (data.battleType === BattleType.TRAINER) { const tObj = data.trainer.toTrainer(this.scene); - const tType = TrainerType[data.trainer.trainerType]; if (data.trainer.trainerType >= 375) { - const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Defeated by Rival", TextStyle.WINDOW); + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:defeatedRival")}`, TextStyle.WINDOW); //otherwise it becomes Rival_5 in Ivy's case this.add(gameOutcomeLabel); + } else if (data.trainer.variant === TrainerVariant.DOUBLE) { + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:defeatedTrainer")+tObj.config.nameDouble+" "+tObj.getName(0, false)}`, TextStyle.WINDOW); + this.add(gameOutcomeLabel); } else { - if (tObj.variant === TrainerVariant.DOUBLE) { - const gameOutcomeLabel = addTextObject(this.scene, 8, 5, "Defeated by Duo", TextStyle.WINDOW); - } - const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `Defeated by ${tObj.getName(0, true)}`, TextStyle.WINDOW); + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:defeatedTrainer")+tObj.getName(0, true)}`, TextStyle.WINDOW); this.add(gameOutcomeLabel); } } } - + const gameModeLabel = addTextObject(this.scene, 8, 19, "", TextStyle.WINDOW); switch (data.gameMode) { - case GameModes.DAILY: - const dailyModeLabel = addTextObject(this.scene, 8, 19, `${i18next.t('gameMode:dailyRun') || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); - this.add(dailyModeLabel); - break; - case GameModes.SPLICED_ENDLESS: - const endlessSplicedLabel = addTextObject(this.scene, 8, 19, `${i18next.t('gameMode:endlessSpliced') || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); - this.add(endlessSplicedLabel); - break; - case GameModes.ENDLESS: - case GameModes.CLASSIC: - case GameModes.CHALLENGE: - const gameModeLabel = addTextObject(this.scene, 8, 19, `${i18next.t('gameMode:'+GameModes[data.gameMode].toLowerCase()) || "Unknown"} - Wave ${data.waveIndex}`, TextStyle.WINDOW); - this.add(gameModeLabel); - break; + case GameModes.DAILY: + gameModeLabel.appendText(`${i18next.t("gameMode:dailyRun")}`, false); + break; + case GameModes.SPLICED_ENDLESS: + gameModeLabel.appendText(`${i18next.t("gameMode:splicedEndless")}`, false); + break; + case GameModes.ENDLESS: + gameModeLabel.appendText(`${i18next.t("gameMode:endless")}`, false); + break; + case GameModes.CLASSIC: + gameModeLabel.appendText(`${i18next.t("gameMode:classic")}`, false); + break; + case GameModes.CHALLENGE: + gameModeLabel.appendText(`${i18next.t("gameMode:challenge")}`, false); + break; } - - const date = new Date(data.timestamp); + gameModeLabel.appendText(" - ", false); + gameModeLabel.appendText(i18next.t("saveSlotSelectUiHandler:wave")+" "+data.waveIndex, false); + this.add(gameModeLabel); const timestampLabel = addTextObject(this.scene, 8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); this.add(timestampLabel); const pokemonIconsContainer = this.scene.add.container(125, 17); - let luckValue = 0; data.party.forEach((p: PokemonData, i: integer) => { const iconContainer = this.scene.add.container(26 * i, 0); @@ -309,7 +309,7 @@ class RunEntry extends Phaser.GameObjects.Container { const pokemon = p.toPokemon(this.scene); const icon = this.scene.addPokemonIcon(pokemon, 0, 0, 0, 0); - const text = addTextObject(this.scene, 32, 20, `Lv${Utils.formatLargeNumber(pokemon.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); + const text = addTextObject(this.scene, 32, 20, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(pokemon.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); text.setShadow(0, 0, null); text.setStroke("#424242", 14); text.setOrigin(1, 0); @@ -319,52 +319,10 @@ class RunEntry extends Phaser.GameObjects.Container { pokemonIconsContainer.add(iconContainer); - luckValue += pokemon.getLuck(); - pokemon.destroy(); }); this.add(pokemonIconsContainer); - - //Display Score - only visible for Daily Mode - //Display Luck - only visible for Endless Modes - switch (data.gameMode) { - case GameModes.DAILY: - const runScore = data.score; - const scoreText = addTextObject(this.scene, 240, 5, `Score: ${data.score}`, TextStyle.WINDOW, {color: "#f89890"}); - this.add(scoreText); - break; - case GameModes.ENDLESS: - case GameModes.SPLICED_ENDLESS: - if (luckValue > 14) { - luckValue = 14; - } - const luckTextTint = "#"+(getLuckTextTint(luckValue)).toString(16); - const luckText = addTextObject(this.scene, 240, 5, `Luck: ${getLuckString(luckValue)}`, TextStyle.WINDOW, {color: `${luckTextTint}`}); - this.add(luckText); - break; - } - /* - const modifiersModule = import("../modifier/modifier"); - - const modifierIconsContainer = this.scene.add.container(148, 30); - modifierIconsContainer.setScale(0.5); - let visibleModifierIndex = 0; - for (const m of data.modifiers) { - const modifier = m.toModifier(this.scene, modifiersModule[m.className]); - if (modifier instanceof PokemonHeldItemModifier) { - continue; - } - const icon = modifier.getIcon(this.scene, false); - icon.setPosition(24 * visibleModifierIndex, 0); - modifierIconsContainer.add(icon); - if (++visibleModifierIndex === 12) { - break; - } - } - - this.add(modifierIconsContainer); - */ } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index b409b7c0d78..c28e3aaeece 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -44,9 +44,9 @@ import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-han import KeyboardBindingUiHandler from "#app/ui/settings/keyboard-binding-ui-handler"; import SettingsDisplayUiHandler from "./settings/settings-display-ui-handler"; import SettingsAudioUiHandler from "./settings/settings-audio-ui-handler"; +import RunHistoryUiHandler from "./run-history-ui-handler"; +import RunInfoUiHandler from "./run-info-ui-handler"; import { PlayerGender } from "#enums/player-gender"; -import BgmBar from "#app/ui/bgm-bar"; -import RenameFormUiHandler from "./rename-form-ui-handler"; export enum Mode { MESSAGE, @@ -74,6 +74,8 @@ export enum Mode { SETTINGS_KEYBOARD, KEYBOARD_BINDING, ACHIEVEMENTS, + RUN_HISTORY, + RUN_INFO, GAME_STATS, VOUCHERS, EGG_LIST, @@ -84,8 +86,7 @@ export enum Mode { SESSION_RELOAD, UNAVAILABLE, OUTDATED, - CHALLENGE_SELECT, - RENAME_POKEMON + CHALLENGE_SELECT } const transitionModes = [ @@ -117,12 +118,12 @@ const noTransitionModes = [ Mode.GAME_STATS, Mode.VOUCHERS, Mode.LOGIN_FORM, + Mode.RUN_HISTORY, Mode.REGISTRATION_FORM, Mode.LOADING, Mode.SESSION_RELOAD, Mode.UNAVAILABLE, - Mode.OUTDATED, - Mode.RENAME_POKEMON + Mode.OUTDATED ]; export default class UI extends Phaser.GameObjects.Container { @@ -131,7 +132,6 @@ export default class UI extends Phaser.GameObjects.Container { public handlers: UiHandler[]; private overlay: Phaser.GameObjects.Rectangle; public achvBar: AchvBar; - public bgmBar: BgmBar; public savingIcon: SavingIconHandler; private tooltipContainer: Phaser.GameObjects.Container; @@ -164,7 +164,6 @@ export default class UI extends Phaser.GameObjects.Container { new OptionSelectUiHandler(scene), new MenuUiHandler(scene), new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT), - // settings new SettingsUiHandler(scene), new SettingsDisplayUiHandler(scene), new SettingsAudioUiHandler(scene), @@ -173,6 +172,8 @@ export default class UI extends Phaser.GameObjects.Container { new SettingsKeyboardUiHandler(scene), new KeyboardBindingUiHandler(scene), new AchvsUiHandler(scene), + new RunHistoryUiHandler(scene), + new RunInfoUiHandler(scene), new GameStatsUiHandler(scene), new VouchersUiHandler(scene), new EggListUiHandler(scene), @@ -183,18 +184,16 @@ export default class UI extends Phaser.GameObjects.Container { new SessionReloadModalUiHandler(scene), new UnavailableModalUiHandler(scene), new OutdatedModalUiHandler(scene), - new GameChallengesUiHandler(scene), - new RenameFormUiHandler(scene), + new GameChallengesUiHandler(scene) ]; } setup(): void { - this.setName(`ui-${Mode[this.mode]}`); + this.setName("container-ui"); for (const handler of this.handlers) { handler.setup(); } this.overlay = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6, 0); - this.overlay.setName("rect-ui-overlay"); this.overlay.setOrigin(0, 0); (this.scene as BattleScene).uiContainer.add(this.overlay); this.overlay.setVisible(false); @@ -213,19 +212,15 @@ export default class UI extends Phaser.GameObjects.Container { private setupTooltip() { this.tooltipContainer = this.scene.add.container(0, 0); - this.tooltipContainer.setName("tooltip"); this.tooltipContainer.setVisible(false); this.tooltipBg = addWindow(this.scene as BattleScene, 0, 0, 128, 31); - this.tooltipBg.setName("window-tooltip-bg"); this.tooltipBg.setOrigin(0, 0); this.tooltipTitle = addTextObject(this.scene, 64, 4, "", TextStyle.TOOLTIP_TITLE); - this.tooltipTitle.setName("text-tooltip-title"); this.tooltipTitle.setOrigin(0.5, 0); this.tooltipContent = addTextObject(this.scene, 6, 16, "", TextStyle.TOOLTIP_CONTENT); - this.tooltipContent.setName("text-tooltip-content"); this.tooltipContent.setWordWrapWidth(696); this.tooltipContainer.add(this.tooltipBg); @@ -235,8 +230,8 @@ export default class UI extends Phaser.GameObjects.Container { (this.scene as BattleScene).uiContainer.add(this.tooltipContainer); } - getHandler(): H { - return this.handlers[this.mode] as H; + getHandler(): UiHandler { + return this.handlers[this.mode]; } getMessageHandler(): BattleMessageUiHandler { @@ -253,6 +248,7 @@ export default class UI extends Phaser.GameObjects.Container { battleScene?.processInfoButton(pressed); return true; } + battleScene?.processInfoButton(false); return true; } @@ -459,6 +455,7 @@ export default class UI extends Phaser.GameObjects.Container { touchControls.dataset.uiMode = Mode[mode]; } this.getHandler().show(args); + console.log(args); } resolve(); }; From 66760d8f0d34c43d9aa38f869cf27575abe22d12 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 08:22:16 -0700 Subject: [PATCH 05/35] 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 { From 379ce1bf724ace4e4c4c9f15331e29b31148f841 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 08:53:29 -0700 Subject: [PATCH 06/35] Some more merging + updating --- src/locales/en/menu-ui-handler.ts | 1 + src/system/game-data.ts | 16 +++++++++------- src/ui/menu-ui-handler.ts | 5 +++++ src/ui/run-history-ui-handler.ts | 1 + src/ui/run-info-ui-handler.ts | 3 +++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/locales/en/menu-ui-handler.ts b/src/locales/en/menu-ui-handler.ts index 6eb680544ed..bc45520a86e 100644 --- a/src/locales/en/menu-ui-handler.ts +++ b/src/locales/en/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Game Settings", "ACHIEVEMENTS": "Achievements", "STATS": "Stats", + "RUN_HISTORY": "Run History", "VOUCHERS": "Vouchers", "EGG_LIST": "Egg List", "EGG_GACHA": "Egg Gacha", diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 08d4bd9c162..98de5040960 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -568,10 +568,10 @@ export class GameData { public async getRunHistoryData(scene: BattleScene): Promise { if (!Utils.isLocal) { - const data = await Utils.apiFetch("savedata/runHistory", true).json(); - //const data = await response.json(); + const response = await Utils.apiFetch("savedata/runHistory", true); + const data = await response.json(); if (localStorage.hasOwnProperty(`runHistoryData_${loggedInUser.username}`)) { - let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`); if (cachedResponse) { cachedResponse = JSON.parse(decrypt(cachedResponse, true)); } @@ -580,13 +580,13 @@ export class GameData { if ( Object.keys(cachedRHData).length >= Object.keys(data).length ) { return cachedRHData; } - } else { - localStorage.setItem(`runHistoryData_${loggedInUser.username}`, JSON.parse(encrypt({}, true))); + } else { + localStorage.setItem(`runHistoryData_${loggedInUser.username}`, JSON.parse(encrypt("", true))); return {}; } return data; } else { - let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`, true); + let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`); if (cachedResponse) { cachedResponse = JSON.parse(decrypt(cachedResponse, true)); } @@ -602,10 +602,12 @@ export class GameData { runHistoryData = {}; } const timestamps = Object.keys(runHistoryData); + const timestampsNo = timestamps.map(Number); //Arbitrary limit of 25 entries per User --> Can increase or decrease if (timestamps.length >= 25) { - delete this.scene.gameData.runHistory[Math.min(timestamps)]; + const oldestTimestamp = Math.min.apply(Math, timestampsNo); + delete this.scene.gameData.runHistory[oldestTimestamp.toString()]; } const timestamp = (runEntry.timestamp).toString(); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index a0cea2f78d5..55f26bc8aa5 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -16,6 +16,7 @@ enum MenuOptions { GAME_SETTINGS, ACHIEVEMENTS, STATS, + RUN_HISTORY, VOUCHERS, EGG_LIST, EGG_GACHA, @@ -325,6 +326,10 @@ export default class MenuUiHandler extends MessageUiHandler { ui.setOverlayMode(Mode.GAME_STATS); success = true; break; + case MenuOptions.RUN_HISTORY: + ui.setOverlayMode(Mode.RUN_HISTORY); + success = true; + break; case MenuOptions.VOUCHERS: ui.setOverlayMode(Mode.VOUCHERS); success = true; diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 9827cd057ac..aa8d9cbf103 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -248,6 +248,7 @@ class RunEntry extends Phaser.GameObjects.Container { const enemyIconContainer = this.scene.add.container(65+(e*25),-8); enemyIconContainer.setScale(0.75); enemyData.boss = false; + enemyData["player"] = true; const enemy = enemyData.toPokemon(this.scene); const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); const enemyLevel = addTextObject(this.scene, 32, 20, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 365db67a05b..b981247abe7 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -162,6 +162,7 @@ export default class GameInfoUiHandler extends UiHandler { const enemyData = runData.enemyParty[0]; const bossStatus = enemyData.boss; enemyData.boss = false; + enemyData["player"] = true; //addPokemonIcon() throws an error if the Pokemon used is a boss const enemy = enemyData.toPokemon(this.scene); const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); @@ -180,6 +181,7 @@ export default class GameInfoUiHandler extends UiHandler { const enemyIconContainer = this.scene.add.container(0, 0); const bossStatus = enemyData.boss; enemyData.boss = false; + enemyData["player"] = true; const enemy = enemyData.toPokemon(this.scene); const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); const enemyLevel = addTextObject(this.scene, 36, 26, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, bossStatus ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "44px", color: "#f8f8f8" }); @@ -230,6 +232,7 @@ export default class GameInfoUiHandler extends UiHandler { enemyIconContainer.setScale(0.6); const isBoss = enemyData.boss; enemyData.boss = false; + enemyData["player"] = true; const enemy = enemyData.toPokemon(this.scene); const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); const enemySprite1 = enemyIcon.list[0] as Phaser.GameObjects.Sprite; From 090b3abc48a08f1e7ed7ef25af2fe0773b56f88c Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 08:56:06 -0700 Subject: [PATCH 07/35] Fixed import --- src/locales/es/run-history-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/es/run-history-ui-handler.ts b/src/locales/es/run-history-ui-handler.ts index 5ba1f69386b..9b666a2e1a4 100644 --- a/src/locales/es/run-history-ui-handler.ts +++ b/src/locales/es/run-history-ui-handler.ts @@ -1,4 +1,4 @@ -import { SimpleTranslationEntries } from "#app/plugins/i18n"; +import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { "victory": "Victory!", From dc56849f60ecdf1269cd355d0f46920b50767455 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 09:01:52 -0700 Subject: [PATCH 08/35] TypeDocs issues --- src/enums/game-data-type.ts | 3 ++- src/locales/en/config.ts | 2 +- src/system/game-data.ts | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/enums/game-data-type.ts b/src/enums/game-data-type.ts index 179817fe5be..d672253794a 100644 --- a/src/enums/game-data-type.ts +++ b/src/enums/game-data-type.ts @@ -6,5 +6,6 @@ export enum GameDataType { SESSION, SETTINGS, TUTORIALS, - SEEN_DIALOGUES + SEEN_DIALOGUES, + RUN_HISTORY } diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 072a6a5bb73..7c47f879ef0 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -39,7 +39,7 @@ import { titles, trainerClasses, trainerNames } from "./trainers"; import { tutorial } from "./tutorial"; import { voucher } from "./voucher"; import { weather } from "./weather"; -import { runHistory } from "./run-history-ui-handler.ts"; +import { runHistory } from "./run-history-ui-handler"; export const enConfig = { ability: ability, diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 98de5040960..987082ffb0c 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -297,6 +297,8 @@ export class GameData { public gameStats: GameStats; + public runHistory: RunHistoryData; + public unlocks: Unlocks; public achvUnlocks: AchvUnlocks; @@ -607,7 +609,7 @@ export class GameData { //Arbitrary limit of 25 entries per User --> Can increase or decrease if (timestamps.length >= 25) { const oldestTimestamp = Math.min.apply(Math, timestampsNo); - delete this.scene.gameData.runHistory[oldestTimestamp.toString()]; + delete runHistoryData[oldestTimestamp.toString()]; } const timestamp = (runEntry.timestamp).toString(); From b8f4174d3fcfe75cc5dbf8053e29bb55f2df17ba Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 10:06:42 -0700 Subject: [PATCH 09/35] Fixed relevant typedoc issues --- src/ui/run-info-ui-handler.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index b981247abe7..61ffb8500bc 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -407,8 +407,8 @@ export default class GameInfoUiHandler extends UiHandler { const pSpecies = pokemon.species; const pNature = getNatureName(pokemon.nature); const pName = pokemon.fusionSpecies ? pokemon.name : pSpecies.name; - const passiveLabel = (currentLanguage==="ko"||currentLanguage==="zh_CN"||currentLanguage==="zh_TW") ? i18next.t("starterSelectUiHandler:passive") : i18next.t("starterSelectUiHandler:passive").charAt(0); - const abilityLabel = (currentLanguage==="ko"||currentLanguage==="zh_CN"||currentLanguage==="zh_TW") ? i18next.t("starterSelectUiHandler:ability") : i18next.t("starterSelectUiHandler:ability").charAt(0); + const passiveLabel = (currentLanguage==="ko"||currentLanguage==="zh_CN"||currentLanguage==="zh_TW") ? i18next.t("starterSelectUiHandler:passive") : (i18next.t("starterSelectUiHandler:passive") as string).charAt(0); + const abilityLabel = (currentLanguage==="ko"||currentLanguage==="zh_CN"||currentLanguage==="zh_TW") ? i18next.t("starterSelectUiHandler:ability") : (i18next.t("starterSelectUiHandler:ability") as string).charAt(0); const pPassiveInfo = pokemon.passive ? `${passiveLabel+": "+allAbilities[starterPassiveAbilities[pSpecies.speciesId]].name}` : ""; const pAbilityInfo = abilityLabel + ": " + pokemon.getAbility().name; const pokeInfoText = addBBCodeTextObject(this.scene, 0, 0, pName, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); @@ -565,11 +565,11 @@ export default class GameInfoUiHandler extends UiHandler { const allContainers = this.partyContainer.getAll("name", "PkmnInfo"); allContainers.forEach((c: Phaser.GameObjects.Container) => { console.log(c.getByName("PkmnMoves")); - c.getByName("PkmnMoves").setVisible(partyVisible); - c.getByName("PkmnInfoText").setVisible(partyVisible); - c.getByName("PkmnStatsText").setVisible(partyVisible); - c.getByName("PkmnMarks").setVisible(partyVisible); - c.getByName("heldItems").setVisible(!partyVisible); + (c.getByName("PkmnMoves") as Phaser.GameObjects.Container).setVisible(partyVisible); + (c.getByName("PkmnInfoText") as Phaser.GameObjects.Container).setVisible(partyVisible); + (c.getByName("PkmnStatsText") as Phaser.GameObjects.Container).setVisible(partyVisible); + (c.getByName("PkmnMarks") as Phaser.GameObjects.Container).setVisible(partyVisible); + (c.getByName("heldItems") as Phaser.GameObjects.Container).setVisible(!partyVisible); this.partyVisibility = partyVisible; }); } From f1df00ef58b1dff7afa738bbf17d611cd944eeaf Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 10:10:53 -0700 Subject: [PATCH 10/35] Manual merge --- src/locales/en/config.ts | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 7c47f879ef0..976d39d736d 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -1,9 +1,16 @@ +import { common } from "./common.js"; +import { settings } from "./settings.js"; import { ability } from "./ability"; import { abilityTriggers } from "./ability-trigger"; +import { arenaFlyout } from "./arena-flyout"; +import { arenaTag } from "./arena-tag"; import { PGFachv, PGMachv } from "./achv"; import { battle } from "./battle"; +import { battleInfo } from "./battle-info"; import { battleMessageUiHandler } from "./battle-message-ui-handler"; +import { battlerTags } from "./battler-tags"; import { berry } from "./berry"; +import { bgmName } from "./bgm-name"; import { biome } from "./biome"; import { challenges } from "./challenges"; import { commandUiHandler } from "./command-ui-handler"; @@ -19,37 +26,49 @@ import { } from "./dialogue"; import { egg } from "./egg"; import { fightUiHandler } from "./fight-ui-handler"; +import { filterBar } from "./filter-bar"; import { gameMode } from "./game-mode"; import { gameStatsUiHandler } from "./game-stats-ui-handler"; import { growth } from "./growth"; import { menu } from "./menu"; import { menuUiHandler } from "./menu-ui-handler"; +import { modifier } from "./modifier"; import { modifierType } from "./modifier-type"; import { move } from "./move"; import { nature } from "./nature"; import { partyUiHandler } from "./party-ui-handler"; import { pokeball } from "./pokeball"; import { pokemon } from "./pokemon"; +import { pokemonForm } from "./pokemon-form"; import { pokemonInfo } from "./pokemon-info"; import { pokemonInfoContainer } from "./pokemon-info-container"; +import { pokemonSummary } from "./pokemon-summary"; import { saveSlotSelectUiHandler } from "./save-slot-select-ui-handler"; import { splashMessages } from "./splash-messages"; import { starterSelectUiHandler } from "./starter-select-ui-handler"; +import { statusEffect } from "./status-effect"; import { titles, trainerClasses, trainerNames } from "./trainers"; import { tutorial } from "./tutorial"; import { voucher } from "./voucher"; -import { weather } from "./weather"; -import { runHistory } from "./run-history-ui-handler"; +import { terrain, weather } from "./weather"; +import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; +import { moveTriggers } from "./move-trigger"; export const enConfig = { ability: ability, abilityTriggers: abilityTriggers, + arenaFlyout: arenaFlyout, + arenaTag: arenaTag, battle: battle, + battleInfo: battleInfo, battleMessageUiHandler: battleMessageUiHandler, + battlerTags: battlerTags, berry: berry, + bgmName: bgmName, biome: biome, challenges: challenges, commandUiHandler: commandUiHandler, + common: common, PGMachv: PGMachv, PGFachv: PGFachv, PGMdialogue: PGMdialogue, @@ -62,27 +81,36 @@ export const enConfig = { PGFdoubleBattleDialogue: PGFdoubleBattleDialogue, egg: egg, fightUiHandler: fightUiHandler, + filterBar: filterBar, gameMode: gameMode, gameStatsUiHandler: gameStatsUiHandler, growth: growth, menu: menu, menuUiHandler: menuUiHandler, + modifier: modifier, modifierType: modifierType, move: move, nature: nature, - partyUiHandler: partyUiHandler, pokeball: pokeball, pokemon: pokemon, + pokemonForm: pokemonForm, pokemonInfo: pokemonInfo, pokemonInfoContainer: pokemonInfoContainer, + pokemonSummary: pokemonSummary, saveSlotSelectUiHandler: saveSlotSelectUiHandler, + settings: settings, splashMessages: splashMessages, starterSelectUiHandler: starterSelectUiHandler, + statusEffect: statusEffect, + terrain: terrain, titles: titles, trainerClasses: trainerClasses, trainerNames: trainerNames, tutorial: tutorial, voucher: voucher, weather: weather, + partyUiHandler: partyUiHandler, + modifierSelectUiHandler: modifierSelectUiHandler, + moveTriggers: moveTriggers, runHistory: runHistory, }; From df967c26824e53d4c26bc436344f849b9f1ecc2e Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 10:17:31 -0700 Subject: [PATCH 11/35] More fixes --- src/locales/en/config.ts | 1 + src/phases.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 976d39d736d..032acad9d8f 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -53,6 +53,7 @@ import { voucher } from "./voucher"; import { terrain, weather } from "./weather"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const enConfig = { ability: ability, diff --git a/src/phases.ts b/src/phases.ts index 17a116e5c42..bbb45b55b17 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -4237,7 +4237,7 @@ export class GameOverPhase extends BattlePhase { } this.scene.gameData.getSession(this.scene.sessionSlotId).then(sessionData => { if (sessionData) { - this.scene.gameData.saveRunHistory(this.scene, sessionData, this.victory, true); + this.scene.gameData.saveRunHistory(this.scene, sessionData, this.victory); } }).catch(err => { console.error(err); From 358101774aefa9fe99f48bac15edc4f377d47bce Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 10:21:11 -0700 Subject: [PATCH 12/35] So many commits for so little --- src/ui/ui.ts | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/ui/ui.ts b/src/ui/ui.ts index c28e3aaeece..2068241b395 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -44,9 +44,11 @@ import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-han import KeyboardBindingUiHandler from "#app/ui/settings/keyboard-binding-ui-handler"; import SettingsDisplayUiHandler from "./settings/settings-display-ui-handler"; import SettingsAudioUiHandler from "./settings/settings-audio-ui-handler"; +import { PlayerGender } from "#enums/player-gender"; +import BgmBar from "#app/ui/bgm-bar"; +import RenameFormUiHandler from "./rename-form-ui-handler"; import RunHistoryUiHandler from "./run-history-ui-handler"; import RunInfoUiHandler from "./run-info-ui-handler"; -import { PlayerGender } from "#enums/player-gender"; export enum Mode { MESSAGE, @@ -74,8 +76,6 @@ export enum Mode { SETTINGS_KEYBOARD, KEYBOARD_BINDING, ACHIEVEMENTS, - RUN_HISTORY, - RUN_INFO, GAME_STATS, VOUCHERS, EGG_LIST, @@ -86,7 +86,10 @@ export enum Mode { SESSION_RELOAD, UNAVAILABLE, OUTDATED, - CHALLENGE_SELECT + CHALLENGE_SELECT, + RENAME_POKEMON, + RUN_HISTORY, + RUN_INFO, } const transitionModes = [ @@ -98,7 +101,8 @@ const transitionModes = [ Mode.EGG_HATCH_SCENE, Mode.EGG_LIST, Mode.EGG_GACHA, - Mode.CHALLENGE_SELECT + Mode.CHALLENGE_SELECT, + Mode.RUN_HISTORY, ]; const noTransitionModes = [ @@ -118,12 +122,12 @@ const noTransitionModes = [ Mode.GAME_STATS, Mode.VOUCHERS, Mode.LOGIN_FORM, - Mode.RUN_HISTORY, Mode.REGISTRATION_FORM, Mode.LOADING, Mode.SESSION_RELOAD, Mode.UNAVAILABLE, - Mode.OUTDATED + Mode.OUTDATED, + Mode.RENAME_POKEMON, ]; export default class UI extends Phaser.GameObjects.Container { @@ -132,6 +136,7 @@ export default class UI extends Phaser.GameObjects.Container { public handlers: UiHandler[]; private overlay: Phaser.GameObjects.Rectangle; public achvBar: AchvBar; + public bgmBar: BgmBar; public savingIcon: SavingIconHandler; private tooltipContainer: Phaser.GameObjects.Container; @@ -164,6 +169,7 @@ export default class UI extends Phaser.GameObjects.Container { new OptionSelectUiHandler(scene), new MenuUiHandler(scene), new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT), + // settings new SettingsUiHandler(scene), new SettingsDisplayUiHandler(scene), new SettingsAudioUiHandler(scene), @@ -172,8 +178,6 @@ export default class UI extends Phaser.GameObjects.Container { new SettingsKeyboardUiHandler(scene), new KeyboardBindingUiHandler(scene), new AchvsUiHandler(scene), - new RunHistoryUiHandler(scene), - new RunInfoUiHandler(scene), new GameStatsUiHandler(scene), new VouchersUiHandler(scene), new EggListUiHandler(scene), @@ -184,16 +188,20 @@ export default class UI extends Phaser.GameObjects.Container { new SessionReloadModalUiHandler(scene), new UnavailableModalUiHandler(scene), new OutdatedModalUiHandler(scene), - new GameChallengesUiHandler(scene) + new GameChallengesUiHandler(scene), + new RenameFormUiHandler(scene), + new RunHistoryUiHandler(scene), + new RunInfoUiHandler(scene), ]; } setup(): void { - this.setName("container-ui"); + this.setName(`ui-${Mode[this.mode]}`); for (const handler of this.handlers) { handler.setup(); } this.overlay = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6, 0); + this.overlay.setName("rect-ui-overlay"); this.overlay.setOrigin(0, 0); (this.scene as BattleScene).uiContainer.add(this.overlay); this.overlay.setVisible(false); @@ -212,15 +220,19 @@ export default class UI extends Phaser.GameObjects.Container { private setupTooltip() { this.tooltipContainer = this.scene.add.container(0, 0); + this.tooltipContainer.setName("tooltip"); this.tooltipContainer.setVisible(false); this.tooltipBg = addWindow(this.scene as BattleScene, 0, 0, 128, 31); + this.tooltipBg.setName("window-tooltip-bg"); this.tooltipBg.setOrigin(0, 0); this.tooltipTitle = addTextObject(this.scene, 64, 4, "", TextStyle.TOOLTIP_TITLE); + this.tooltipTitle.setName("text-tooltip-title"); this.tooltipTitle.setOrigin(0.5, 0); this.tooltipContent = addTextObject(this.scene, 6, 16, "", TextStyle.TOOLTIP_CONTENT); + this.tooltipContent.setName("text-tooltip-content"); this.tooltipContent.setWordWrapWidth(696); this.tooltipContainer.add(this.tooltipBg); @@ -230,8 +242,8 @@ export default class UI extends Phaser.GameObjects.Container { (this.scene as BattleScene).uiContainer.add(this.tooltipContainer); } - getHandler(): UiHandler { - return this.handlers[this.mode]; + getHandler(): H { + return this.handlers[this.mode] as H; } getMessageHandler(): BattleMessageUiHandler { @@ -248,7 +260,6 @@ export default class UI extends Phaser.GameObjects.Container { battleScene?.processInfoButton(pressed); return true; } - battleScene?.processInfoButton(false); return true; } @@ -455,7 +466,6 @@ export default class UI extends Phaser.GameObjects.Container { touchControls.dataset.uiMode = Mode[mode]; } this.getHandler().show(args); - console.log(args); } resolve(); }; From 72314f88dd3fb65e75419c1db4119ca808d49d21 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 15:11:42 -0700 Subject: [PATCH 13/35] Localization Updates --- src/locales/de/config.ts | 4 +++- src/locales/de/run-history-ui-handler.ts | 4 ++++ src/locales/es/config.ts | 4 +++- src/locales/es/run-history-ui-handler.ts | 5 +++++ src/locales/fr/config.ts | 4 +++- src/locales/fr/run-history-ui-handler.ts | 4 ++++ src/locales/it/config.ts | 4 +++- src/locales/it/run-history-ui-handler.ts | 4 ++++ src/locales/ko/config.ts | 4 +++- src/locales/ko/run-history-ui-handler.ts | 4 ++++ src/locales/pt_BR/config.ts | 4 +++- src/locales/pt_BR/run-history-ui-handler.ts | 4 ++++ src/locales/zh_CN/config.ts | 4 +++- src/locales/zh_CN/run-history-ui-handler.ts | 4 ++++ src/locales/zh_TW/config.ts | 4 +++- src/locales/zh_TW/run-history-ui-handler.ts | 4 ++++ 16 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/locales/de/config.ts b/src/locales/de/config.ts index b01f8343b29..8f5f0f0fe74 100644 --- a/src/locales/de/config.ts +++ b/src/locales/de/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const deConfig = { ability: ability, @@ -111,5 +112,6 @@ export const deConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/de/run-history-ui-handler.ts +++ b/src/locales/de/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/es/config.ts b/src/locales/es/config.ts index ed2477aa14d..2b8c1f71b13 100644 --- a/src/locales/es/config.ts +++ b/src/locales/es/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const esConfig = { ability: ability, @@ -111,5 +112,6 @@ export const esConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/es/run-history-ui-handler.ts b/src/locales/es/run-history-ui-handler.ts index 9b666a2e1a4..76b0102e5ef 100644 --- a/src/locales/es/run-history-ui-handler.ts +++ b/src/locales/es/run-history-ui-handler.ts @@ -21,9 +21,14 @@ export const runHistory: SimpleTranslationEntries = { "challengeMonoGen8":"Gen VIII", "challengeMonoGen9":"Gen IX", "playerItems":"Player Items", + "personalBest":"Personal Best!", "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/fr/config.ts b/src/locales/fr/config.ts index 153e9edcf1d..dfbb6b0be08 100644 --- a/src/locales/fr/config.ts +++ b/src/locales/fr/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const frConfig = { ability: ability, @@ -111,5 +112,6 @@ export const frConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/fr/run-history-ui-handler.ts b/src/locales/fr/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/fr/run-history-ui-handler.ts +++ b/src/locales/fr/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/it/config.ts b/src/locales/it/config.ts index a6cb95c20eb..d11a016b4be 100644 --- a/src/locales/it/config.ts +++ b/src/locales/it/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const itConfig = { ability: ability, @@ -111,5 +112,6 @@ export const itConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/it/run-history-ui-handler.ts b/src/locales/it/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/it/run-history-ui-handler.ts +++ b/src/locales/it/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/ko/config.ts b/src/locales/ko/config.ts index f3017dd350e..4ee745dffd2 100644 --- a/src/locales/ko/config.ts +++ b/src/locales/ko/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const koConfig = { ability: ability, @@ -111,5 +112,6 @@ export const koConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/ko/run-history-ui-handler.ts b/src/locales/ko/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/ko/run-history-ui-handler.ts +++ b/src/locales/ko/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/pt_BR/config.ts b/src/locales/pt_BR/config.ts index ce4576646c2..efe5b2937cc 100644 --- a/src/locales/pt_BR/config.ts +++ b/src/locales/pt_BR/config.ts @@ -53,6 +53,7 @@ import { tutorial } from "./tutorial"; import { voucher } from "./voucher"; import { terrain, weather } from "./weather"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const ptBrConfig = { ability: ability, @@ -111,5 +112,6 @@ export const ptBrConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/pt_BR/run-history-ui-handler.ts b/src/locales/pt_BR/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/pt_BR/run-history-ui-handler.ts +++ b/src/locales/pt_BR/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/zh_CN/config.ts b/src/locales/zh_CN/config.ts index 8f00168c58c..10208cd2168 100644 --- a/src/locales/zh_CN/config.ts +++ b/src/locales/zh_CN/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const zhCnConfig = { ability: ability, @@ -111,5 +112,6 @@ export const zhCnConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/zh_CN/run-history-ui-handler.ts b/src/locales/zh_CN/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/zh_CN/run-history-ui-handler.ts +++ b/src/locales/zh_CN/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts diff --git a/src/locales/zh_TW/config.ts b/src/locales/zh_TW/config.ts index 7a974d58fc3..21fa30cdfc1 100644 --- a/src/locales/zh_TW/config.ts +++ b/src/locales/zh_TW/config.ts @@ -53,6 +53,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const zhTwConfig = { ability: ability, @@ -111,5 +112,6 @@ export const zhTwConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/zh_TW/run-history-ui-handler.ts b/src/locales/zh_TW/run-history-ui-handler.ts index d0f622599e2..76b0102e5ef 100644 --- a/src/locales/zh_TW/run-history-ui-handler.ts +++ b/src/locales/zh_TW/run-history-ui-handler.ts @@ -25,6 +25,10 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vel.", "runInfo":"Run Info", "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameText":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!" } as const; //Mode Information found in game-mode.ts From eaee12e3dcd4c2681898501998bc0428e99dc8eb Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 15:15:16 -0700 Subject: [PATCH 14/35] Very barebones implementation of 'favorite' runs - not planning to implement any time soon though --- src/system/game-data.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 987082ffb0c..11e20f3847c 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -616,6 +616,8 @@ export class GameData { runHistoryData[timestamp] = {}; runHistoryData[timestamp]["entry"] = runEntry; runHistoryData[timestamp]["victory"] = victory; + //Not implemented at the moment, but leaving room for future work + runHistoryData[timestamp]["favorite"] = false; localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt(JSON.stringify(runHistoryData), true)); From cf3397126edb8cd39fce3bb799f5aed468f8b5df Mon Sep 17 00:00:00 2001 From: Frutescens Date: Tue, 30 Jul 2024 15:19:27 -0700 Subject: [PATCH 15/35] menu-ui-handler.ts localizations --- src/locales/de/menu-ui-handler.ts | 1 + src/locales/es/menu-ui-handler.ts | 1 + src/locales/fr/menu-ui-handler.ts | 1 + src/locales/it/menu-ui-handler.ts | 1 + src/locales/ko/menu-ui-handler.ts | 1 + src/locales/pt_BR/menu-ui-handler.ts | 1 + src/locales/zh_CN/menu-ui-handler.ts | 1 + src/locales/zh_TW/menu-ui-handler.ts | 1 + 8 files changed, 8 insertions(+) diff --git a/src/locales/de/menu-ui-handler.ts b/src/locales/de/menu-ui-handler.ts index 6d461d286af..090b50c4661 100644 --- a/src/locales/de/menu-ui-handler.ts +++ b/src/locales/de/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Spieleinstellungen", "ACHIEVEMENTS": "Erfolge", "STATS": "Statistiken", + "RUN_HISTORY": "Run History", "VOUCHERS": "Gutscheine", "EGG_LIST": "Eierliste", "EGG_GACHA": "Eier-Gacha", diff --git a/src/locales/es/menu-ui-handler.ts b/src/locales/es/menu-ui-handler.ts index e38ac4eab3d..9867b77be57 100644 --- a/src/locales/es/menu-ui-handler.ts +++ b/src/locales/es/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Ajustes", "ACHIEVEMENTS": "Logros", "STATS": "Estadísticas", + "RUN_HISTORY": "Run History", "VOUCHERS": "Vales", "EGG_LIST": "Lista de Huevos", "EGG_GACHA": "Gacha de Huevos", diff --git a/src/locales/fr/menu-ui-handler.ts b/src/locales/fr/menu-ui-handler.ts index 70cf05fe984..c9ca74ea8dc 100644 --- a/src/locales/fr/menu-ui-handler.ts +++ b/src/locales/fr/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Paramètres", "ACHIEVEMENTS": "Succès", "STATS": "Statistiques", + "RUN_HISTORY": "Run History", "VOUCHERS": "Coupons", "EGG_LIST": "Liste des Œufs", "EGG_GACHA": "Gacha-Œufs", diff --git a/src/locales/it/menu-ui-handler.ts b/src/locales/it/menu-ui-handler.ts index 58e48574476..47c72151758 100644 --- a/src/locales/it/menu-ui-handler.ts +++ b/src/locales/it/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Impostazioni", "ACHIEVEMENTS": "Obiettivi", "STATS": "Statistiche", + "RUN_HISTORY": "Run History", "VOUCHERS": "Biglietti", "EGG_LIST": "Lista uova", "EGG_GACHA": "Macchine uova", diff --git a/src/locales/ko/menu-ui-handler.ts b/src/locales/ko/menu-ui-handler.ts index 2e036f49b4d..4b928807e29 100644 --- a/src/locales/ko/menu-ui-handler.ts +++ b/src/locales/ko/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "게임 설정", "ACHIEVEMENTS": "업적", "STATS": "통계", + "RUN_HISTORY": "Run History", "VOUCHERS": "바우처", "EGG_LIST": "알 목록", "EGG_GACHA": "알 뽑기", diff --git a/src/locales/pt_BR/menu-ui-handler.ts b/src/locales/pt_BR/menu-ui-handler.ts index b874df7e23b..05c1ed9a3ab 100644 --- a/src/locales/pt_BR/menu-ui-handler.ts +++ b/src/locales/pt_BR/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Configurações", "ACHIEVEMENTS": "Conquistas", "STATS": "Estatísticas", + "RUN_HISTORY": "Run History", "VOUCHERS": "Vouchers", "EGG_LIST": "Incubadora", "EGG_GACHA": "Gacha de ovos", diff --git a/src/locales/zh_CN/menu-ui-handler.ts b/src/locales/zh_CN/menu-ui-handler.ts index e5ca6d977df..1f1487c6531 100644 --- a/src/locales/zh_CN/menu-ui-handler.ts +++ b/src/locales/zh_CN/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "游戏设置", "ACHIEVEMENTS": "成就", "STATS": "数据统计", + "RUN_HISTORY": "Run History", "VOUCHERS": "兑换券", "EGG_LIST": "蛋列表", "EGG_GACHA": "扭蛋机", diff --git a/src/locales/zh_TW/menu-ui-handler.ts b/src/locales/zh_TW/menu-ui-handler.ts index e3675b0571e..a41d7c9f12e 100644 --- a/src/locales/zh_TW/menu-ui-handler.ts +++ b/src/locales/zh_TW/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "遊戲設置", "ACHIEVEMENTS": "成就", "STATS": "數據", + "RUN_HISTORY": "Run History", "VOUCHERS": "兌換劵", "EGG_LIST": "蛋列表", "EGG_GACHA": "扭蛋機", From 232ee0f2bfdb4e7362d2815504619328e08bf2b1 Mon Sep 17 00:00:00 2001 From: Mumble Date: Tue, 30 Jul 2024 20:44:35 -0700 Subject: [PATCH 16/35] Update src/locales/ko/run-history-ui-handler.ts Thank you! Co-authored-by: Enoch --- src/locales/ko/run-history-ui-handler.ts | 56 ++++++++++++------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/locales/ko/run-history-ui-handler.ts b/src/locales/ko/run-history-ui-handler.ts index 76b0102e5ef..7b8dd4e5c5f 100644 --- a/src/locales/ko/run-history-ui-handler.ts +++ b/src/locales/ko/run-history-ui-handler.ts @@ -1,34 +1,34 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { - "victory": "Victory!", - "defeatedWild": "Defeated by ", - "defeatedTrainer": "Defeated by ", - "defeatedTrainerDouble": "Defeated by Duo", - "defeatedRival": "Defeated by Rival", - "defeated":"Defeated", - "luck":"Luck", - "score":"Score", - "mode":"Mode", - "challengeRules":"Rule(s)", - "challengeMonoGen1":"Gen I", - "challengeMonoGen2":"Gen II", - "challengeMonoGen3":"Gen III", - "challengeMonoGen4":"Gen IV", - "challengeMonoGen5":"Gen V", - "challengeMonoGen6":"Gen VI", - "challengeMonoGen7":"Gen VII", - "challengeMonoGen8":"Gen VIII", - "challengeMonoGen9":"Gen IX", - "playerItems":"Player Items", - "personalBest":"Personal Best!", - "SPDshortened":"Vel.", - "runInfo":"Run Info", - "money":"Money", - "runLength":"Run Length", - "viewHeldItems":"Held Items", - "hallofFameText":"Welcome to the Hall of Fame!", - "viewHallOfFame":"View Hall of Fame!" + "victory": "승리!", + "defeatedWild": "야생에서 패배: ", + "defeatedTrainer": "트레이너에게 패배: ", + "defeatedTrainerDouble": "더블 배틀에서 패배", + "defeatedRival": "라이벌에게 패배", + "defeated":"패배", + "luck":"행운", + "score":"점수", + "mode":"모드", + "challengeRules":"규칙", + "challengeMonoGen1":"1세대", + "challengeMonoGen2":"2세대", + "challengeMonoGen3":"3세대", + "challengeMonoGen4":"4세대", + "challengeMonoGen5":"5세대", + "challengeMonoGen6":"6세대", + "challengeMonoGen7":"7세대", + "challengeMonoGen8":"8세대", + "challengeMonoGen9":"9세대", + "playerItems":"플레이어 아이템", + "personalBest":"개인 최고기록!", + "SPDshortened":"스피드", + "runInfo":"플레이 정보", + "money":"소지금", + "runLength":"플레이 타임", + "viewHeldItems":"도구", + "hallofFameText":"전당 등록을 축하합니다!", + "viewHallOfFame":"전당 보기" } as const; //Mode Information found in game-mode.ts From c4be01699a76e33f47a3d20b5f8a11ad14c9d772 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 11:53:55 -0700 Subject: [PATCH 17/35] Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- src/locales/de/run-history-ui-handler.ts | 38 ++++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts index 76b0102e5ef..03b81352adc 100644 --- a/src/locales/de/run-history-ui-handler.ts +++ b/src/locales/de/run-history-ui-handler.ts @@ -1,16 +1,16 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { - "victory": "Victory!", - "defeatedWild": "Defeated by ", - "defeatedTrainer": "Defeated by ", - "defeatedTrainerDouble": "Defeated by Duo", - "defeatedRival": "Defeated by Rival", - "defeated":"Defeated", - "luck":"Luck", - "score":"Score", - "mode":"Mode", - "challengeRules":"Rule(s)", + "victory": "Sieg!", + "defeatedWild": "Besiegt durch ", + "defeatedTrainer": "Besiegt durch", + "defeatedTrainerDouble": "Besiegt durch Doppelkampf", + "defeatedRival": "Besiegt durch Rivale", + "defeated":"Besiegt", + "luck":"Glück", + "score":"Punkte", + "mode":"Modus", + "challengeRules":"Regeln", "challengeMonoGen1":"Gen I", "challengeMonoGen2":"Gen II", "challengeMonoGen3":"Gen III", @@ -20,15 +20,15 @@ export const runHistory: SimpleTranslationEntries = { "challengeMonoGen7":"Gen VII", "challengeMonoGen8":"Gen VIII", "challengeMonoGen9":"Gen IX", - "playerItems":"Player Items", - "personalBest":"Personal Best!", - "SPDshortened":"Vel.", - "runInfo":"Run Info", - "money":"Money", - "runLength":"Run Length", - "viewHeldItems":"Held Items", - "hallofFameText":"Welcome to the Hall of Fame!", - "viewHallOfFame":"View Hall of Fame!" + "playerItems":"Spielergegenstände", + "personalBest":"Persönlicher Bestwert!", + "SPDshortened":"Geschw.", + "runInfo":"Durchlauf Info.", + "money":"Geld", + "runLength":"Länge des Durchlaufs, + "viewHeldItems":"Getragene Items", + "hallofFameText":"Willkommen in der Ruhmeshalle", + "viewHallOfFame":"Ruhmeshalle ansehen!" } as const; //Mode Information found in game-mode.ts From 5bccedce91f9f9cee07810daba43f32d82a3ee02 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 11:54:04 -0700 Subject: [PATCH 18/35] Update src/locales/de/menu-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- src/locales/de/menu-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/de/menu-ui-handler.ts b/src/locales/de/menu-ui-handler.ts index 090b50c4661..598512487ba 100644 --- a/src/locales/de/menu-ui-handler.ts +++ b/src/locales/de/menu-ui-handler.ts @@ -4,7 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Spieleinstellungen", "ACHIEVEMENTS": "Erfolge", "STATS": "Statistiken", - "RUN_HISTORY": "Run History", + "RUN_HISTORY": "Laufhistorie", "VOUCHERS": "Gutscheine", "EGG_LIST": "Eierliste", "EGG_GACHA": "Eier-Gacha", From 990cbc1aae650ce9b689a144c023ac8b1cc6fca8 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Wed, 31 Jul 2024 12:12:22 -0700 Subject: [PATCH 19/35] The German name for Squirtle is silly --- src/locales/de/run-history-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts index 03b81352adc..57583c4d7bd 100644 --- a/src/locales/de/run-history-ui-handler.ts +++ b/src/locales/de/run-history-ui-handler.ts @@ -25,7 +25,7 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Geschw.", "runInfo":"Durchlauf Info.", "money":"Geld", - "runLength":"Länge des Durchlaufs, + "runLength":"Länge des Durchlaufs", "viewHeldItems":"Getragene Items", "hallofFameText":"Willkommen in der Ruhmeshalle", "viewHallOfFame":"Ruhmeshalle ansehen!" From 4e94621da40601be4874f38bf0c6440c1677fd4f Mon Sep 17 00:00:00 2001 From: Frutescens Date: Wed, 31 Jul 2024 12:14:59 -0700 Subject: [PATCH 20/35] Fixed Run-Entry Def --- src/system/game-data.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 11e20f3847c..e8368b5651a 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -189,6 +189,7 @@ export interface RunHistoryData { export interface RunEntries { entry: SessionSaveData; victory: boolean; + favorite: boolean; } export type StarterMoveset = [ Moves ] | [ Moves, Moves ] | [ Moves, Moves, Moves ] | [ Moves, Moves, Moves, Moves ]; From ba44238f8f5b8d5f535267969f607139d159daa6 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Wed, 31 Jul 2024 13:16:06 -0700 Subject: [PATCH 21/35] Commented out networking functionality --- src/system/game-data.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index e8368b5651a..64657c1b456 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -571,8 +571,12 @@ export class GameData { public async getRunHistoryData(scene: BattleScene): Promise { if (!Utils.isLocal) { + /** + * Networking Code + * const response = await Utils.apiFetch("savedata/runHistory", true); const data = await response.json(); + */ if (localStorage.hasOwnProperty(`runHistoryData_${loggedInUser.username}`)) { let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`); if (cachedResponse) { @@ -622,6 +626,9 @@ export class GameData { localStorage.setItem(`runHistoryData_${loggedInUser.username}`, encrypt(JSON.stringify(runHistoryData), true)); + /** + * Networking Code + * if (!Utils.isLocal) { try { Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); @@ -631,6 +638,7 @@ export class GameData { return false; } } + */ } parseSystemData(dataStr: string): SystemSaveData { From f00b84951c1303653b085c87390e4967dc5c8d12 Mon Sep 17 00:00:00 2001 From: Frutescens Date: Wed, 31 Jul 2024 13:22:24 -0700 Subject: [PATCH 22/35] Commenting out network functionality pt2 --- src/system/game-data.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 64657c1b456..fd7b1bc806e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -584,14 +584,19 @@ export class GameData { } const cachedRHData = cachedResponse ?? {}; //check to see whether cachedData or serverData is more up-to-date + /** + * Networking Code + * if ( Object.keys(cachedRHData).length >= Object.keys(data).length ) { return cachedRHData; } + */ + return cachedRHData; } else { localStorage.setItem(`runHistoryData_${loggedInUser.username}`, JSON.parse(encrypt("", true))); return {}; } - return data; + //return data; } else { let cachedResponse = localStorage.getItem(`runHistoryData_${loggedInUser.username}`); if (cachedResponse) { @@ -639,6 +644,7 @@ export class GameData { } } */ + return true; } parseSystemData(dataStr: string): SystemSaveData { From e4100367929c36a0545eb01cefea0874415e7671 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 16:03:49 -0700 Subject: [PATCH 23/35] Update src/locales/pt_BR/menu-ui-handler.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Ricardo Fleury Oliveira --- src/locales/pt_BR/menu-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/pt_BR/menu-ui-handler.ts b/src/locales/pt_BR/menu-ui-handler.ts index 05c1ed9a3ab..f8c5ff5a8ad 100644 --- a/src/locales/pt_BR/menu-ui-handler.ts +++ b/src/locales/pt_BR/menu-ui-handler.ts @@ -4,7 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Configurações", "ACHIEVEMENTS": "Conquistas", "STATS": "Estatísticas", - "RUN_HISTORY": "Run History", + "RUN_HISTORY": "Histórico de Jogos", "VOUCHERS": "Vouchers", "EGG_LIST": "Incubadora", "EGG_GACHA": "Gacha de ovos", From 4dab13eb83004048c1ea6d617278b9c9216d9473 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 16:03:59 -0700 Subject: [PATCH 24/35] Update src/locales/pt_BR/run-history-ui-handler.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Ricardo Fleury Oliveira --- src/locales/pt_BR/run-history-ui-handler.ts | 56 ++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/locales/pt_BR/run-history-ui-handler.ts b/src/locales/pt_BR/run-history-ui-handler.ts index 76b0102e5ef..787d8987326 100644 --- a/src/locales/pt_BR/run-history-ui-handler.ts +++ b/src/locales/pt_BR/run-history-ui-handler.ts @@ -1,34 +1,34 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { - "victory": "Victory!", - "defeatedWild": "Defeated by ", - "defeatedTrainer": "Defeated by ", - "defeatedTrainerDouble": "Defeated by Duo", - "defeatedRival": "Defeated by Rival", - "defeated":"Defeated", - "luck":"Luck", - "score":"Score", - "mode":"Mode", - "challengeRules":"Rule(s)", - "challengeMonoGen1":"Gen I", - "challengeMonoGen2":"Gen II", - "challengeMonoGen3":"Gen III", - "challengeMonoGen4":"Gen IV", - "challengeMonoGen5":"Gen V", - "challengeMonoGen6":"Gen VI", - "challengeMonoGen7":"Gen VII", - "challengeMonoGen8":"Gen VIII", - "challengeMonoGen9":"Gen IX", - "playerItems":"Player Items", - "personalBest":"Personal Best!", - "SPDshortened":"Vel.", - "runInfo":"Run Info", - "money":"Money", - "runLength":"Run Length", - "viewHeldItems":"Held Items", - "hallofFameText":"Welcome to the Hall of Fame!", - "viewHallOfFame":"View Hall of Fame!" + "victory": "Vitória!", + "defeatedWild": "Derrotado por ", + "defeatedTrainer": "Derrotado por ", + "defeatedTrainerDouble": "Derrotado por Dupla", + "defeatedRival": "Derrotado por Rival", + "defeated": "Derrotado", + "luck": "Sorte", + "score": "Pontuação", + "mode": "Modo", + "challengeRules": "Regra(s)", + "challengeMonoGen1": "Ger. 1", + "challengeMonoGen2": "Ger. 2", + "challengeMonoGen3": "Ger. 3", + "challengeMonoGen4": "Ger. 4", + "challengeMonoGen5": "Ger. 5", + "challengeMonoGen6": "Ger. 6", + "challengeMonoGen7": "Ger. 7", + "challengeMonoGen8": "Ger. 8", + "challengeMonoGen9": "Ger. 9", + "playerItems": "Itens do Jogador", + "personalBest": "Recorde Pessoal!", + "SPDshortened": "Vel.", + "runInfo": "Info. do Jogo", + "money": "Dinheiro", + "runLength": "Duração do Jogo", + "viewHeldItems": "Itens Segurados", + "hallofFameText": "Bem-vindo(a) ao Hall da Fama!", + "viewHallOfFame": "Veja o Hall da Fama!" } as const; //Mode Information found in game-mode.ts From f4cf72ecab085c10ce97bc1f71724c60748dfc73 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 18:37:59 -0700 Subject: [PATCH 25/35] Update src/locales/ko/menu-ui-handler.ts Co-authored-by: Enoch --- src/locales/ko/menu-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ko/menu-ui-handler.ts b/src/locales/ko/menu-ui-handler.ts index 4b928807e29..de64f3748ab 100644 --- a/src/locales/ko/menu-ui-handler.ts +++ b/src/locales/ko/menu-ui-handler.ts @@ -4,7 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "게임 설정", "ACHIEVEMENTS": "업적", "STATS": "통계", - "RUN_HISTORY": "Run History", + "RUN_HISTORY": "플레이 이력", "VOUCHERS": "바우처", "EGG_LIST": "알 목록", "EGG_GACHA": "알 뽑기", From e7ac37a7e3a647b1816e7e05def6ee88f3b29cc6 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 18:38:08 -0700 Subject: [PATCH 26/35] Update src/locales/zh_CN/menu-ui-handler.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> --- src/locales/zh_CN/menu-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/zh_CN/menu-ui-handler.ts b/src/locales/zh_CN/menu-ui-handler.ts index 1f1487c6531..3367ca11036 100644 --- a/src/locales/zh_CN/menu-ui-handler.ts +++ b/src/locales/zh_CN/menu-ui-handler.ts @@ -4,7 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "游戏设置", "ACHIEVEMENTS": "成就", "STATS": "数据统计", - "RUN_HISTORY": "Run History", + "RUN_HISTORY": "历史记录", "VOUCHERS": "兑换券", "EGG_LIST": "蛋列表", "EGG_GACHA": "扭蛋机", From f8aa15e4520e37000d48fdcd5bd94ffdf5f285e1 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 18:38:21 -0700 Subject: [PATCH 27/35] Update src/locales/zh_CN/run-history-ui-handler.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> --- src/locales/zh_CN/run-history-ui-handler.ts | 56 ++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/locales/zh_CN/run-history-ui-handler.ts b/src/locales/zh_CN/run-history-ui-handler.ts index 76b0102e5ef..3215634e1fc 100644 --- a/src/locales/zh_CN/run-history-ui-handler.ts +++ b/src/locales/zh_CN/run-history-ui-handler.ts @@ -1,34 +1,34 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { - "victory": "Victory!", - "defeatedWild": "Defeated by ", - "defeatedTrainer": "Defeated by ", - "defeatedTrainerDouble": "Defeated by Duo", - "defeatedRival": "Defeated by Rival", - "defeated":"Defeated", - "luck":"Luck", - "score":"Score", - "mode":"Mode", - "challengeRules":"Rule(s)", - "challengeMonoGen1":"Gen I", - "challengeMonoGen2":"Gen II", - "challengeMonoGen3":"Gen III", - "challengeMonoGen4":"Gen IV", - "challengeMonoGen5":"Gen V", - "challengeMonoGen6":"Gen VI", - "challengeMonoGen7":"Gen VII", - "challengeMonoGen8":"Gen VIII", - "challengeMonoGen9":"Gen IX", - "playerItems":"Player Items", - "personalBest":"Personal Best!", - "SPDshortened":"Vel.", - "runInfo":"Run Info", - "money":"Money", - "runLength":"Run Length", - "viewHeldItems":"Held Items", - "hallofFameText":"Welcome to the Hall of Fame!", - "viewHallOfFame":"View Hall of Fame!" + "victory": "胜利!", + "defeatedWild": "被打败", + "defeatedTrainer": "被打败", + "defeatedTrainerDouble": "被组合打败", + "defeatedRival": "被劲敌打败", + "defeated":"被打败", + "luck":"幸运", + "score":"分数", + "mode":"模式", + "challengeRules":"规则", + "challengeMonoGen1":"一代", + "challengeMonoGen2":"二代", + "challengeMonoGen3":"三代", + "challengeMonoGen4":"四代", + "challengeMonoGen5":"五代", + "challengeMonoGen6":"六代", + "challengeMonoGen7":"七代", + "challengeMonoGen8":"八代", + "challengeMonoGen9":"九代", + "playerItems":"玩家道具", + "personalBest":"个人最佳!", + "SPDshortened":"速率", + "runInfo":"游戏记录", + "money":"金钱", + "runLength":"游戏", + "viewHeldItems":"持有道具", + "hallofFameText":"欢迎来到名人堂!", + "viewHallOfFame":"浏览名人堂!" } as const; //Mode Information found in game-mode.ts From 24ee15a881781d4fa29398983540a83af8294dc3 Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 18:38:35 -0700 Subject: [PATCH 28/35] Update src/locales/fr/menu-ui-handler.ts Co-authored-by: Lugiad' --- src/locales/fr/menu-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/fr/menu-ui-handler.ts b/src/locales/fr/menu-ui-handler.ts index c9ca74ea8dc..22bb1863b8b 100644 --- a/src/locales/fr/menu-ui-handler.ts +++ b/src/locales/fr/menu-ui-handler.ts @@ -4,7 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Paramètres", "ACHIEVEMENTS": "Succès", "STATS": "Statistiques", - "RUN_HISTORY": "Run History", + "RUN_HISTORY": "Historique", "VOUCHERS": "Coupons", "EGG_LIST": "Liste des Œufs", "EGG_GACHA": "Gacha-Œufs", From daa37f0b510df94d51e2e8d847ffcf3169395a5d Mon Sep 17 00:00:00 2001 From: Mumble Date: Wed, 31 Jul 2024 18:38:42 -0700 Subject: [PATCH 29/35] Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' --- src/locales/fr/run-history-ui-handler.ts | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/locales/fr/run-history-ui-handler.ts b/src/locales/fr/run-history-ui-handler.ts index 76b0102e5ef..16b4b84408d 100644 --- a/src/locales/fr/run-history-ui-handler.ts +++ b/src/locales/fr/run-history-ui-handler.ts @@ -1,34 +1,34 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { - "victory": "Victory!", - "defeatedWild": "Defeated by ", - "defeatedTrainer": "Defeated by ", - "defeatedTrainerDouble": "Defeated by Duo", - "defeatedRival": "Defeated by Rival", - "defeated":"Defeated", - "luck":"Luck", + "victory": "Victoire !", + "defeatedWild": "Battu par ", + "defeatedTrainer": "Battu par ", + "defeatedTrainerDouble": "Battu par Duo", + "defeatedRival": "Battu par Rival", + "defeated":"Vaincu", + "luck":"Chance", "score":"Score", "mode":"Mode", - "challengeRules":"Rule(s)", - "challengeMonoGen1":"Gen I", - "challengeMonoGen2":"Gen II", - "challengeMonoGen3":"Gen III", - "challengeMonoGen4":"Gen IV", - "challengeMonoGen5":"Gen V", - "challengeMonoGen6":"Gen VI", - "challengeMonoGen7":"Gen VII", - "challengeMonoGen8":"Gen VIII", - "challengeMonoGen9":"Gen IX", - "playerItems":"Player Items", - "personalBest":"Personal Best!", - "SPDshortened":"Vel.", - "runInfo":"Run Info", - "money":"Money", - "runLength":"Run Length", - "viewHeldItems":"Held Items", - "hallofFameText":"Welcome to the Hall of Fame!", - "viewHallOfFame":"View Hall of Fame!" + "challengeRules":"Règle·s", + "challengeMonoGen1":"1G", + "challengeMonoGen2":"2G", + "challengeMonoGen3":"3G", + "challengeMonoGen4":"4G", + "challengeMonoGen5":"5G", + "challengeMonoGen6":"6G", + "challengeMonoGen7":"7G", + "challengeMonoGen8":"8G", + "challengeMonoGen9":"9G", + "playerItems":"Objets joueur", + "personalBest":"Record personnel !", + "SPDshortened":"Vit.", + "runInfo":"Infos session", + "money":"Argent", + "runLength":"Durée session", + "viewHeldItems":"Objets tenus", + "hallofFameText":"Bienvenue au Panthéon !", + "viewHallOfFame":"Voir le Panthéon !" } as const; //Mode Information found in game-mode.ts From 78b78dd78a441db72856fa5f19717255083d4f18 Mon Sep 17 00:00:00 2001 From: Mumble Date: Thu, 1 Aug 2024 07:52:35 -0700 Subject: [PATCH 30/35] Update src/locales/it/menu-ui-handler.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> --- src/locales/it/menu-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/it/menu-ui-handler.ts b/src/locales/it/menu-ui-handler.ts index 47c72151758..3cab3b4c790 100644 --- a/src/locales/it/menu-ui-handler.ts +++ b/src/locales/it/menu-ui-handler.ts @@ -4,7 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Impostazioni", "ACHIEVEMENTS": "Obiettivi", "STATS": "Statistiche", - "RUN_HISTORY": "Run History", + "RUN_HISTORY": "Run precedenti", "VOUCHERS": "Biglietti", "EGG_LIST": "Lista uova", "EGG_GACHA": "Macchine uova", From fa480219d4a7dd12c0205a8a4968f87445442e19 Mon Sep 17 00:00:00 2001 From: Mumble Date: Thu, 1 Aug 2024 07:52:46 -0700 Subject: [PATCH 31/35] Update src/locales/it/run-history-ui-handler.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> --- src/locales/it/run-history-ui-handler.ts | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/locales/it/run-history-ui-handler.ts b/src/locales/it/run-history-ui-handler.ts index 76b0102e5ef..84e2b339059 100644 --- a/src/locales/it/run-history-ui-handler.ts +++ b/src/locales/it/run-history-ui-handler.ts @@ -1,34 +1,34 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { - "victory": "Victory!", - "defeatedWild": "Defeated by ", - "defeatedTrainer": "Defeated by ", - "defeatedTrainerDouble": "Defeated by Duo", - "defeatedRival": "Defeated by Rival", - "defeated":"Defeated", - "luck":"Luck", - "score":"Score", - "mode":"Mode", - "challengeRules":"Rule(s)", - "challengeMonoGen1":"Gen I", - "challengeMonoGen2":"Gen II", - "challengeMonoGen3":"Gen III", - "challengeMonoGen4":"Gen IV", - "challengeMonoGen5":"Gen V", - "challengeMonoGen6":"Gen VI", - "challengeMonoGen7":"Gen VII", - "challengeMonoGen8":"Gen VIII", - "challengeMonoGen9":"Gen IX", - "playerItems":"Player Items", - "personalBest":"Personal Best!", + "victory": "Vittoria!", + "defeatedWild": "Sconfitti da ", + "defeatedTrainer": "Sconfitti da ", + "defeatedTrainerDouble": "Sconfitti dalla coppia ", + "defeatedRival": "Sconfitti dal rivale", + "defeated":"Sconfitti", + "luck":"Fortuna", + "score":"Punteggio", + "mode":"Modalità", + "challengeRules":"Regola/e", + "challengeMonoGen1":"1ª gen", + "challengeMonoGen2":"2ª gen", + "challengeMonoGen3":"3ª gen", + "challengeMonoGen4":"4ª gen", + "challengeMonoGen5":"5ª gen", + "challengeMonoGen6":"6ª gen", + "challengeMonoGen7":"7ª gen", + "challengeMonoGen8":"8ª gen", + "challengeMonoGen9":"9ª gen", + "playerItems":"Oggetti giocatore", + "personalBest":"Record personale!", "SPDshortened":"Vel.", - "runInfo":"Run Info", - "money":"Money", - "runLength":"Run Length", - "viewHeldItems":"Held Items", - "hallofFameText":"Welcome to the Hall of Fame!", - "viewHallOfFame":"View Hall of Fame!" + "runInfo":"Info Run", + "money":"Patrimonio", + "runLength":"Durata Run", + "viewHeldItems":"Oggetti equipaggiati ", + "hallofFameText":"Benvenuto alla Sala d'Onore!", + "viewHallOfFame":"Visualizza Sala d'Onore!" } as const; //Mode Information found in game-mode.ts From 9b103d3ee7c8f5b9295e128d742a57ab16b27f6f Mon Sep 17 00:00:00 2001 From: Mumble Date: Thu, 1 Aug 2024 10:15:33 -0700 Subject: [PATCH 32/35] Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' --- src/locales/fr/run-history-ui-handler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/locales/fr/run-history-ui-handler.ts b/src/locales/fr/run-history-ui-handler.ts index 16b4b84408d..01392128aea 100644 --- a/src/locales/fr/run-history-ui-handler.ts +++ b/src/locales/fr/run-history-ui-handler.ts @@ -7,10 +7,10 @@ export const runHistory: SimpleTranslationEntries = { "defeatedTrainerDouble": "Battu par Duo", "defeatedRival": "Battu par Rival", "defeated":"Vaincu", - "luck":"Chance", + "luck":"Chance ", "score":"Score", - "mode":"Mode", - "challengeRules":"Règle·s", + "mode":"Mode ", + "challengeRules":"Règles ", "challengeMonoGen1":"1G", "challengeMonoGen2":"2G", "challengeMonoGen3":"3G", @@ -25,7 +25,7 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Vit.", "runInfo":"Infos session", "money":"Argent", - "runLength":"Durée session", + "runLength":"Durée session ", "viewHeldItems":"Objets tenus", "hallofFameText":"Bienvenue au Panthéon !", "viewHallOfFame":"Voir le Panthéon !" From 1452ac50c4680e3dee2bce7c784ff8b1bf11b402 Mon Sep 17 00:00:00 2001 From: Mumble Date: Fri, 2 Aug 2024 10:59:11 -0700 Subject: [PATCH 33/35] Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- src/locales/de/run-history-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts index 57583c4d7bd..bc9a69fc07f 100644 --- a/src/locales/de/run-history-ui-handler.ts +++ b/src/locales/de/run-history-ui-handler.ts @@ -3,7 +3,7 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales"; export const runHistory: SimpleTranslationEntries = { "victory": "Sieg!", "defeatedWild": "Besiegt durch ", - "defeatedTrainer": "Besiegt durch", + "defeatedTrainer": "Besiegt durch ", "defeatedTrainerDouble": "Besiegt durch Doppelkampf", "defeatedRival": "Besiegt durch Rivale", "defeated":"Besiegt", From 85049944aeddc36245ba83e233f3fbd4aeba7364 Mon Sep 17 00:00:00 2001 From: Mumble Date: Fri, 2 Aug 2024 10:59:24 -0700 Subject: [PATCH 34/35] Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- src/locales/de/run-history-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts index bc9a69fc07f..4541f859faf 100644 --- a/src/locales/de/run-history-ui-handler.ts +++ b/src/locales/de/run-history-ui-handler.ts @@ -23,7 +23,7 @@ export const runHistory: SimpleTranslationEntries = { "playerItems":"Spielergegenstände", "personalBest":"Persönlicher Bestwert!", "SPDshortened":"Geschw.", - "runInfo":"Durchlauf Info.", + "runInfo":"Durchlauf Informationen", "money":"Geld", "runLength":"Länge des Durchlaufs", "viewHeldItems":"Getragene Items", From 1f66aa48c3087e48302360407f83db30e342062f Mon Sep 17 00:00:00 2001 From: Mumble Date: Fri, 2 Aug 2024 11:00:23 -0700 Subject: [PATCH 35/35] Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- src/locales/de/run-history-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts index 4541f859faf..f4cd83f1c7c 100644 --- a/src/locales/de/run-history-ui-handler.ts +++ b/src/locales/de/run-history-ui-handler.ts @@ -25,7 +25,7 @@ export const runHistory: SimpleTranslationEntries = { "SPDshortened":"Geschw.", "runInfo":"Durchlauf Informationen", "money":"Geld", - "runLength":"Länge des Durchlaufs", + "runLength":"Durchlauf Dauer", "viewHeldItems":"Getragene Items", "hallofFameText":"Willkommen in der Ruhmeshalle", "viewHallOfFame":"Ruhmeshalle ansehen!"