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", "name": "pokemon-rogue-battle",
"version": "1.4.0", "version": "1.4.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.4.0", "version": "1.4.1",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.2.7", "@material/material-color-utilities": "^0.2.7",

View File

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

View File

@ -31,7 +31,7 @@ import { BerryType } from "#enums/berry-type";
import { PERMANENT_STATS, Stat } from "#enums/stat"; import { PERMANENT_STATS, Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; 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 */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound"; const namespace = "mysteryEncounters/berriesAbound";
@ -62,8 +62,8 @@ export const BerriesAboundEncounter: MysteryEncounter =
let bossSpecies: PokemonSpecies; let bossSpecies: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!); const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
bossSpecies = allSpecies[eventEncounter.species]; const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
bossSpecies.speciesId = bossSpecies.getSpeciesForLevel(level, eventEncounter.allowEvolution); bossSpecies = getPokemonSpecies( levelSpecies );
} else { } else {
bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); 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 { randSeedInt, randSeedItem } from "#app/utils";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; 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 */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fightOrFlight"; const namespace = "mysteryEncounters/fightOrFlight";
@ -60,8 +60,8 @@ export const FightOrFlightEncounter: MysteryEncounter =
let bossSpecies: PokemonSpecies; let bossSpecies: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!); const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
bossSpecies = allSpecies[eventEncounter.species]; const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
bossSpecies.speciesId = bossSpecies.getSpeciesForLevel(level, eventEncounter.allowEvolution); bossSpecies = getPokemonSpecies( levelSpecies );
} else { } else {
bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); 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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; 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 */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed"; const namespace = "mysteryEncounters/uncommonBreed";
@ -55,8 +55,8 @@ export const UncommonBreedEncounter: MysteryEncounter =
let species: PokemonSpecies; let species: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!); const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
species = allSpecies[eventEncounter.species]; const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
species.speciesId = species.getSpeciesForLevel(level, eventEncounter.allowEvolution); species = getPokemonSpecies( levelSpecies );
} else { } else {
species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); 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 { BattleType } from "#app/battle";
import TrainerData from "#app/system/trainer-data"; import TrainerData from "#app/system/trainer-data";
import { trainerConfigs } from "#app/data/trainer-config"; 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 { achvs } from "#app/system/achv";
import EggData from "#app/system/egg-data"; import EggData from "#app/system/egg-data";
import { Egg } from "#app/data/egg"; import { Egg } from "#app/data/egg";
@ -65,16 +65,12 @@ export const defaultStarterSpecies: Species[] = [
const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary 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) { switch (dataType) {
case GameDataType.SYSTEM: case GameDataType.SYSTEM:
return "data"; return `data_${username}`;
case GameDataType.SESSION: case GameDataType.SESSION:
let ret = "sessionData"; return `sessionData${slotId || ""}_${username}`;
if (slotId) {
ret += slotId;
}
return ret;
case GameDataType.SETTINGS: case GameDataType.SETTINGS:
return "settings"; return "settings";
case GameDataType.TUTORIALS: case GameDataType.TUTORIALS:
@ -82,7 +78,7 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str
case GameDataType.SEEN_DIALOGUES: case GameDataType.SEEN_DIALOGUES:
return "seenDialogues"; return "seenDialogues";
case GameDataType.RUN_HISTORY: 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> { public tryExportData(dataType: GameDataType, slotId: integer = 0): Promise<boolean> {
return new Promise<boolean>(resolve => { 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) => { const handleData = (dataStr: string) => {
switch (dataType) { switch (dataType) {
case GameDataType.SYSTEM: case GameDataType.SYSTEM:
@ -1381,38 +1403,56 @@ export class GameData {
link.click(); link.click();
link.remove(); link.remove();
}; };
if (!bypassLogin && dataType < GameDataType.SETTINGS) { this.getDataToExport(dataType, slotId)
let promise: Promise<string | null> = Promise.resolve(null); .then(data => {
if (data) {
if (dataType === GameDataType.SYSTEM) { handleData(data);
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;
} }
resolve(!!data);
handleData(response);
resolve(true);
}); });
} else {
const data = localStorage.getItem(dataKey);
if (data) {
handleData(decrypt(data, bypassLogin));
}
resolve(!!data);
}
}); });
} }
public importData(dataType: GameDataType, slotId: integer = 0): void { public validateDataToImport(dataStr: string, dataType: GameDataType): boolean {
const dataKey = `${getDataTypeKey(dataType, slotId)}_${loggedInUser?.username}`; 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"); let saveFile: any = document.getElementById("saveFile");
if (saveFile) { if (saveFile) {
saveFile.remove(); saveFile.remove();
@ -1429,41 +1469,13 @@ export class GameData {
reader.onload = (_ => { reader.onload = (_ => {
return e => { return e => {
let dataName: string; const dataName = (dataType === GameDataType.RUN_HISTORY)
let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct? ? i18next.t("menuUiHandler:RUN_HISTORY").toLowerCase()
let valid = false; : GameDataType[dataType].toLowerCase();
try { const dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct?
dataName = GameDataType[dataType].toLowerCase(); const valid = this.validateDataToImport(dataStr, dataType);
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 displayError = (error: string) => this.scene.ui.showText(error, null, () => this.scene.ui.showText("", 0), Utils.fixedInt(1500)); 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) { 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)); 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.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => {
this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { this.scene.ui.setOverlayMode(Mode.CONFIRM, () => {
localStorage.setItem(dataKey, encrypt(dataStr, bypassLogin)); this.setImportedData(dataStr, dataType, slotId);
if (!bypassLogin && dataType < GameDataType.SETTINGS) { if (!bypassLogin && dataType < GameDataType.SETTINGS) {
updateUserInfo().then(success => { 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 { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { SessionSaveData } from "#app/system/game-data"; import { SessionSaveData } from "#app/system/game-data";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { GameDataType } from "#enums/game-data-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
@ -73,4 +74,63 @@ describe("System - Game Data", () => {
expect(account.updateUserInfo).toHaveBeenCalled(); 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 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) { if (Utils.isLocal || Utils.isBeta) {
manageDataOptions.push({ manageDataOptions.push({
label: i18next.t("menuUiHandler:importData"), label: i18next.t("menuUiHandler:importData"),