Compare commits

...

14 Commits

Author SHA1 Message Date
Philippe ba0a3f8395
Merge 7c57ccf42e into 10e0f9f0de 2024-12-20 23:41:58 -08:00
NightKev 10e0f9f0de
Merge pull request #5024 from pagefaultgames/main
Merge main to beta
2024-12-20 19:32:30 -08:00
AJ Fontaine 8457fb96fe
[Hotfix] Fix off-by-one error for event encounters (#5022)
* Fix off-by-one error for event encounters

* Increment version to 1.4.1

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2024-12-20 18:41:07 -08:00
damocleas f95a5d41cb
Merge pull request #5019 from pagefaultgames/beta
Release 1.4.0
2024-12-20 19:06:24 -05:00
Tempoanon 6d903440b4
Release 1.3.0
Release 1.3.0
2024-12-03 18:05:48 -05:00
NightKev 7c57ccf42e
Merge branch 'beta' into feat/export-settings 2024-11-27 23:29:12 -08:00
prateau 02ad35a15e Merge branch 'beta' into feat/export-settings 2024-11-25 22:01:16 +01:00
Adrian T. f88aaadbe7
Merge branch 'beta' into feat/export-settings 2024-11-04 08:07:51 +08:00
prateau 7625093ed9 Merge branch 'beta' into feat/export-settings 2024-11-01 23:24:44 +01:00
prateau 9312545deb add tests on import / export settings 2024-10-06 14:28:30 +02:00
prateau 6a0605c781 fix eslint 2024-10-05 12:20:51 +02:00
prateau 6d49080ca5 Fix bad merge 2024-10-05 12:12:42 +02:00
prateau 02940aa516 Merge branch 'beta' into feat/export-settings 2024-10-05 12:01:07 +02:00
prateau 9f04d7d423 implement import/export settings 2024-09-21 20:00:29 +02:00
8 changed files with 170 additions and 82 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "pokemon-rogue-battle",
"version": "1.4.0",
"version": "1.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pokemon-rogue-battle",
"version": "1.4.0",
"version": "1.4.1",
"hasInstallScript": true,
"dependencies": {
"@material/material-color-utilities": "^0.2.7",

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.4.0",
"version": "1.4.1",
"type": "module",
"scripts": {
"start": "vite",

View File

@ -31,7 +31,7 @@ import { BerryType } from "#enums/berry-type";
import { PERMANENT_STATS, Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import PokemonSpecies, { allSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound";
@ -62,8 +62,8 @@ export const BerriesAboundEncounter: MysteryEncounter =
let bossSpecies: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
bossSpecies = allSpecies[eventEncounter.species];
bossSpecies.speciesId = bossSpecies.getSpeciesForLevel(level, eventEncounter.allowEvolution);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
bossSpecies = getPokemonSpecies( levelSpecies );
} else {
bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
}

View File

@ -29,7 +29,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { randSeedInt, randSeedItem } from "#app/utils";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import PokemonSpecies, { allSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fightOrFlight";
@ -60,8 +60,8 @@ export const FightOrFlightEncounter: MysteryEncounter =
let bossSpecies: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
bossSpecies = allSpecies[eventEncounter.species];
bossSpecies.speciesId = bossSpecies.getSpeciesForLevel(level, eventEncounter.allowEvolution);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
bossSpecies = getPokemonSpecies( levelSpecies );
} else {
bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
}

View File

@ -23,7 +23,7 @@ import { BerryModifier } from "#app/modifier/modifier";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import PokemonSpecies, { allSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed";
@ -55,8 +55,8 @@ export const UncommonBreedEncounter: MysteryEncounter =
let species: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
species = allSpecies[eventEncounter.species];
species.speciesId = species.getSpeciesForLevel(level, eventEncounter.allowEvolution);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
species = getPokemonSpecies( levelSpecies );
} else {
species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
}

View File

@ -14,7 +14,7 @@ import { GameModes, getGameMode } from "#app/game-mode";
import { BattleType } from "#app/battle";
import TrainerData from "#app/system/trainer-data";
import { trainerConfigs } from "#app/data/trainer-config";
import { resetSettings, setSetting, SettingKeys } from "#app/system/settings/settings";
import { resetSettings, setSetting, Setting, settingIndex, SettingKeys } from "#app/system/settings/settings";
import { achvs } from "#app/system/achv";
import EggData from "#app/system/egg-data";
import { Egg } from "#app/data/egg";
@ -65,16 +65,12 @@ export const defaultStarterSpecies: Species[] = [
const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary
export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): string {
export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0, username?: string): string {
switch (dataType) {
case GameDataType.SYSTEM:
return "data";
return `data_${username}`;
case GameDataType.SESSION:
let ret = "sessionData";
if (slotId) {
ret += slotId;
}
return ret;
return `sessionData${slotId || ""}_${username}`;
case GameDataType.SETTINGS:
return "settings";
case GameDataType.TUTORIALS:
@ -82,7 +78,7 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str
case GameDataType.SEEN_DIALOGUES:
return "seenDialogues";
case GameDataType.RUN_HISTORY:
return "runHistoryData";
return `runHistoryData_${username}`;
}
}
@ -1364,9 +1360,35 @@ export class GameData {
});
}
public getDataToExport(dataType: GameDataType, slotId: integer = 0): Promise<string | null> {
return new Promise<string | null>(resolve => {
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
let promise: Promise<string | null> = Promise.resolve(null);
if (dataType === GameDataType.SYSTEM) {
promise = pokerogueApi.savedata.system.get({ clientSessionId });
} else if (dataType === GameDataType.SESSION) {
promise = pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
}
promise.then(response => {
if (!response?.length || response[0] !== "{") {
console.error(response);
resolve(null);
}
resolve(response);
});
} else {
const dataKey: string = getDataTypeKey(dataType, slotId, loggedInUser?.username);
const data = localStorage.getItem(dataKey);
resolve((!data || dataType === GameDataType.SETTINGS) ? data : decrypt(data, bypassLogin));
}
});
}
public tryExportData(dataType: GameDataType, slotId: integer = 0): Promise<boolean> {
return new Promise<boolean>(resolve => {
const dataKey: string = `${getDataTypeKey(dataType, slotId)}_${loggedInUser?.username}`;
const dataKey: string = getDataTypeKey(dataType, slotId, loggedInUser?.username);
const handleData = (dataStr: string) => {
switch (dataType) {
case GameDataType.SYSTEM:
@ -1381,38 +1403,56 @@ export class GameData {
link.click();
link.remove();
};
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
let promise: Promise<string | null> = Promise.resolve(null);
if (dataType === GameDataType.SYSTEM) {
promise = pokerogueApi.savedata.system.get({ clientSessionId });
} else if (dataType === GameDataType.SESSION) {
promise = pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
}
promise.then(response => {
if (!response?.length || response[0] !== "{") {
console.error(response);
resolve(false);
return;
this.getDataToExport(dataType, slotId)
.then(data => {
if (data) {
handleData(data);
}
handleData(response);
resolve(true);
resolve(!!data);
});
} else {
const data = localStorage.getItem(dataKey);
if (data) {
handleData(decrypt(data, bypassLogin));
}
resolve(!!data);
}
});
}
public importData(dataType: GameDataType, slotId: integer = 0): void {
const dataKey = `${getDataTypeKey(dataType, slotId)}_${loggedInUser?.username}`;
public validateDataToImport(dataStr: string, dataType: GameDataType): boolean {
try {
switch (dataType) {
case GameDataType.SYSTEM:
dataStr = this.convertSystemDataStr(dataStr);
const systemData = this.parseSystemData(dataStr);
return !!systemData.dexData && !!systemData.timestamp;
case GameDataType.SESSION:
const sessionData = this.parseSessionData(dataStr);
return !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp;
case GameDataType.RUN_HISTORY:
const data = JSON.parse(dataStr);
const keys = Object.keys(data);
return keys.every((key) => {
const entryKeys = Object.keys(data[key]);
return [ "isFavorite", "isVictory", "entry" ].every(v => entryKeys.includes(v)) && entryKeys.length === 3;
});
case GameDataType.SETTINGS:
return Object.entries(JSON.parse(dataStr))
.every(([ k, v ]: [string, number]) => {
const index: number = settingIndex(k);
return index === -1 || Setting[index].options.length > v;
});
case GameDataType.TUTORIALS:
case GameDataType.SEEN_DIALOGUES:
return true;
}
} catch (ex) {
console.error(ex);
return false;
}
}
public setImportedData(dataStr: string, dataType: GameDataType, slotId: integer = 0) {
const dataKey = getDataTypeKey(dataType, slotId, loggedInUser?.username);
const encryptedData = (dataType === GameDataType.SETTINGS) ? dataStr : encrypt(dataStr, bypassLogin);
localStorage.setItem(dataKey, encryptedData);
}
public importData(dataType: GameDataType, slotId: integer = 0): void {
let saveFile: any = document.getElementById("saveFile");
if (saveFile) {
saveFile.remove();
@ -1429,41 +1469,13 @@ export class GameData {
reader.onload = (_ => {
return e => {
let dataName: string;
let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct?
let valid = false;
try {
dataName = GameDataType[dataType].toLowerCase();
switch (dataType) {
case GameDataType.SYSTEM:
dataStr = this.convertSystemDataStr(dataStr);
const systemData = this.parseSystemData(dataStr);
valid = !!systemData.dexData && !!systemData.timestamp;
break;
case GameDataType.SESSION:
const sessionData = this.parseSessionData(dataStr);
valid = !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp;
break;
case GameDataType.RUN_HISTORY:
const data = JSON.parse(dataStr);
const keys = Object.keys(data);
dataName = i18next.t("menuUiHandler:RUN_HISTORY").toLowerCase();
keys.forEach((key) => {
const entryKeys = Object.keys(data[key]);
valid = [ "isFavorite", "isVictory", "entry" ].every(v => entryKeys.includes(v)) && entryKeys.length === 3;
});
break;
case GameDataType.SETTINGS:
case GameDataType.TUTORIALS:
valid = true;
break;
}
} catch (ex) {
console.error(ex);
}
const dataName = (dataType === GameDataType.RUN_HISTORY)
? i18next.t("menuUiHandler:RUN_HISTORY").toLowerCase()
: GameDataType[dataType].toLowerCase();
const dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct?
const valid = this.validateDataToImport(dataStr, dataType);
const displayError = (error: string) => this.scene.ui.showText(error, null, () => this.scene.ui.showText("", 0), Utils.fixedInt(1500));
dataName = dataName!; // tell TS compiler that dataName is defined!
if (!valid) {
return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText("", 0), Utils.fixedInt(1500));
@ -1471,7 +1483,7 @@ export class GameData {
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));
this.setImportedData(dataStr, dataType, slotId);
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
updateUserInfo().then(success => {

View File

@ -2,6 +2,7 @@ import * as BattleScene from "#app/battle-scene";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { SessionSaveData } from "#app/system/game-data";
import { Abilities } from "#enums/abilities";
import { GameDataType } from "#enums/game-data-type";
import { Moves } from "#enums/moves";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
@ -73,4 +74,63 @@ describe("System - Game Data", () => {
expect(account.updateUserInfo).toHaveBeenCalled();
});
});
describe("getDataToExport", () => {
it("should get default settings", async () => {
const defaultSettings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}";
localStorage.setItem("settings", defaultSettings);
const result = await game.scene.gameData.getDataToExport(GameDataType.SETTINGS);
expect(result).toEqual(defaultSettings);
});
it("should get undefined when there is no settings", async () => {
const result = await game.scene.gameData.getDataToExport(GameDataType.SETTINGS);
expect(result).toBeUndefined();
});
});
describe("setImportedData", () => {
it("should set settings in local storage", () => {
const settings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}";
game.scene.gameData.setImportedData(settings, GameDataType.SETTINGS);
expect(localStorage.getItem("settings")).toEqual(settings);
});
it("should override default settings", () => {
const defaultSettings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}";
localStorage.setItem("settings", defaultSettings);
const newSettings = "{\"PLAYER_GENDER\":1,\"gameVersion\":\"1.0.7\",\"GAME_SPEED\":7}";
game.scene.gameData.setImportedData(newSettings, GameDataType.SETTINGS);
expect(localStorage.getItem("settings")).toEqual(newSettings);
});
});
describe("validateDataToImport", () => {
it("should be true when the setting data is valid", async () => {
const settings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\"}";
const result = await game.scene.gameData.validateDataToImport(settings, GameDataType.SETTINGS);
expect(result).toBeTruthy();
});
it("should be false when the setting data is an invalid JSON", async () => {
const settings = "";
const result = await game.scene.gameData.validateDataToImport(settings, GameDataType.SETTINGS);
expect(result).toBeFalsy();
});
it("should be false when the setting data contains an unknow value", async () => {
const settings = "{\"PLAYER_GENDER\":0,\"gameVersion\":\"1.0.4\",\"GAME_SPEED\":999}";
const result = await game.scene.gameData.validateDataToImport(settings, GameDataType.SETTINGS);
expect(result).toBeFalsy();
});
});
});

View File

@ -246,6 +246,22 @@ export default class MenuUiHandler extends MessageUiHandler {
},
keepOpen: true
});
manageDataOptions.push({
label: i18next.t("menuUiHandler:importSettings"),
handler: () => {
this.scene.gameData.importData(GameDataType.SETTINGS);
return true;
},
keepOpen: true
});
manageDataOptions.push({
label: i18next.t("menuUiHandler:exportSettings"),
handler: () => {
this.scene.gameData.tryExportData(GameDataType.SETTINGS);
return true;
},
keepOpen: true
});
if (Utils.isLocal || Utils.isBeta) {
manageDataOptions.push({
label: i18next.t("menuUiHandler:importData"),