mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 17:12:44 +02:00
Added version migrator for rage fist data + deepMergeSpriteData tests
This commit is contained in:
parent
c3db9e3532
commit
079d50e2e0
@ -7,7 +7,6 @@ import type PokemonSpecies from "#app/data/pokemon-species";
|
|||||||
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
|
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
|
||||||
import {
|
import {
|
||||||
fixedInt,
|
fixedInt,
|
||||||
deepMergeObjects,
|
|
||||||
getIvsFromId,
|
getIvsFromId,
|
||||||
randSeedInt,
|
randSeedInt,
|
||||||
getEnumValues,
|
getEnumValues,
|
||||||
@ -19,6 +18,7 @@ import {
|
|||||||
BooleanHolder,
|
BooleanHolder,
|
||||||
type Constructor,
|
type Constructor,
|
||||||
} from "#app/utils/common";
|
} from "#app/utils/common";
|
||||||
|
import { deepMergeSpriteData } from "#app/utils/data";
|
||||||
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
||||||
import {
|
import {
|
||||||
ConsumableModifier,
|
ConsumableModifier,
|
||||||
@ -788,7 +788,7 @@ export default class BattleScene extends SceneBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const expVariantData = await this.cachedFetch("./images/pokemon/variant/_exp_masterlist.json").then(r => r.json());
|
const expVariantData = await this.cachedFetch("./images/pokemon/variant/_exp_masterlist.json").then(r => r.json());
|
||||||
deepMergeObjects(variantData, expVariantData);
|
deepMergeSpriteData(variantData, expVariantData);
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedFetch(url: string, init?: RequestInit): Promise<Response> {
|
cachedFetch(url: string, init?: RequestInit): Promise<Response> {
|
||||||
@ -836,6 +836,7 @@ export default class BattleScene extends SceneBase {
|
|||||||
return this.getPlayerField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false));
|
return this.getPlayerField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add `undefined` to return type
|
||||||
/**
|
/**
|
||||||
* Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not.
|
* Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not.
|
||||||
* Does not actually check if the pokemon are on the field or not.
|
* Does not actually check if the pokemon are on the field or not.
|
||||||
@ -851,9 +852,9 @@ export default class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField on the field}
|
* @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField | on the field}
|
||||||
* and {@linkcode EnemyPokemon.isActive is active}
|
* and {@linkcode EnemyPokemon.isActive | is active}
|
||||||
* (aka {@linkcode EnemyPokemon.isAllowedInBattle is allowed in battle}),
|
* (aka {@linkcode EnemyPokemon.isAllowedInBattle | is allowed in battle}),
|
||||||
* or `undefined` if there are no valid pokemon
|
* or `undefined` if there are no valid pokemon
|
||||||
* @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true`
|
* @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true`
|
||||||
*/
|
*/
|
||||||
@ -1298,14 +1299,13 @@ export default class BattleScene extends SceneBase {
|
|||||||
return Math.max(doubleChance.value, 1);
|
return Math.max(doubleChance.value, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ...this never actually returns `null`, right?
|
|
||||||
newBattle(
|
newBattle(
|
||||||
waveIndex?: number,
|
waveIndex?: number,
|
||||||
battleType?: BattleType,
|
battleType?: BattleType,
|
||||||
trainerData?: TrainerData,
|
trainerData?: TrainerData,
|
||||||
double?: boolean,
|
double?: boolean,
|
||||||
mysteryEncounterType?: MysteryEncounterType,
|
mysteryEncounterType?: MysteryEncounterType,
|
||||||
): Battle | null {
|
): Battle {
|
||||||
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
|
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
|
||||||
const newWaveIndex = waveIndex || (this.currentBattle?.waveIndex || _startingWave - 1) + 1;
|
const newWaveIndex = waveIndex || (this.currentBattle?.waveIndex || _startingWave - 1) + 1;
|
||||||
let newDouble: boolean | undefined;
|
let newDouble: boolean | undefined;
|
||||||
@ -1492,7 +1492,7 @@ export default class BattleScene extends SceneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const pokemon of this.getPlayerParty()) {
|
for (const pokemon of this.getPlayerParty()) {
|
||||||
pokemon.resetBattleData();
|
pokemon.resetBattleAndWaveData();
|
||||||
pokemon.resetTera();
|
pokemon.resetTera();
|
||||||
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
||||||
if (
|
if (
|
||||||
@ -3261,6 +3261,7 @@ export default class BattleScene extends SceneBase {
|
|||||||
[this.modifierBar, this.enemyModifierBar].map(m => m.setVisible(visible));
|
[this.modifierBar, this.enemyModifierBar].map(m => m.setVisible(visible));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Document this - IDK what it does and it gets called a lot
|
||||||
updateModifiers(player = true, instant?: boolean): void {
|
updateModifiers(player = true, instant?: boolean): void {
|
||||||
const modifiers = player ? this.modifiers : (this.enemyModifiers as PersistentModifier[]);
|
const modifiers = player ? this.modifiers : (this.enemyModifiers as PersistentModifier[]);
|
||||||
for (let m = 0; m < modifiers.length; m++) {
|
for (let m = 0; m < modifiers.length; m++) {
|
||||||
@ -3313,8 +3314,8 @@ export default class BattleScene extends SceneBase {
|
|||||||
* gets removed. This function does NOT apply in-battle effects, such as Unburden.
|
* gets removed. This function does NOT apply in-battle effects, such as Unburden.
|
||||||
* If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead.
|
* If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead.
|
||||||
* @param modifier The item to be removed.
|
* @param modifier The item to be removed.
|
||||||
* @param enemy If `true`, remove an item owned by the enemy. If `false`, remove an item owned by the player. Default is `false`.
|
* @param enemy `true` to remove an item owned by the enemy rather than the player; default `false`.
|
||||||
* @returns `true` if the item exists and was successfully removed, `false` otherwise.
|
* @returns `true` if the item exists and was successfully removed, `false` otherwise
|
||||||
*/
|
*/
|
||||||
removeModifier(modifier: PersistentModifier, enemy = false): boolean {
|
removeModifier(modifier: PersistentModifier, enemy = false): boolean {
|
||||||
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
|
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
|
||||||
|
@ -8,15 +8,19 @@ import type { Nature } from "#enums/nature";
|
|||||||
* Includes abilities, nature, changed types, etc.
|
* Includes abilities, nature, changed types, etc.
|
||||||
*/
|
*/
|
||||||
export class CustomPokemonData {
|
export class CustomPokemonData {
|
||||||
public spriteScale = -1;
|
public spriteScale: number;
|
||||||
public ability: Abilities | -1 = -1;
|
public ability: Abilities | -1;
|
||||||
public passive: Abilities | -1 = -1;
|
public passive: Abilities | -1;
|
||||||
public nature: Nature | -1 = -1;
|
public nature: Nature | -1;
|
||||||
public types: PokemonType[] = [];
|
public types: PokemonType[];
|
||||||
|
|
||||||
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
|
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
|
||||||
if (!isNullOrUndefined(data)) {
|
if (!isNullOrUndefined(data)) {
|
||||||
Object.assign(this, data);
|
this.spriteScale = data.spriteScale ?? 1;
|
||||||
|
this.ability = data.ability ?? -1;
|
||||||
|
this.passive = data.passive || data.spriteScale;
|
||||||
|
this.spriteScale = this.spriteScale || data.spriteScale;
|
||||||
|
this.types = data.types || this.types;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import { SpeciesFormKey } from "#enums/species-form-key";
|
|||||||
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
||||||
import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite";
|
import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite";
|
||||||
import { hasExpSprite } from "#app/sprites/sprite-utils";
|
import { hasExpSprite } from "#app/sprites/sprite-utils";
|
||||||
|
import { Gender } from "./gender";
|
||||||
|
|
||||||
export enum Region {
|
export enum Region {
|
||||||
NORMAL,
|
NORMAL,
|
||||||
@ -846,6 +847,23 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick and return a random {@linkcode Gender} for a {@linkcode Pokemon}.
|
||||||
|
* @param id The personality value of the pokemon being generated.
|
||||||
|
* @returns THe selected gender for this Pokemon, rolled based on its PID.
|
||||||
|
*/
|
||||||
|
generateGender(id: number): Gender {
|
||||||
|
if (isNullOrUndefined(this.malePercent)) {
|
||||||
|
return Gender.GENDERLESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const genderChance = (id % 256) * 0.390625;
|
||||||
|
if (genderChance < this.malePercent) {
|
||||||
|
return Gender.MALE;
|
||||||
|
}
|
||||||
|
return Gender.FEMALE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the name of species with proper attachments for regionals and separate starter forms (Floette, Ursaluna)
|
* Find the name of species with proper attachments for regionals and separate starter forms (Floette, Ursaluna)
|
||||||
* @returns a string with the region name or other form name attached
|
* @returns a string with the region name or other form name attached
|
||||||
|
@ -334,7 +334,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
/* Pokemon data types, in vague order of precedence */
|
/* Pokemon data types, in vague order of precedence */
|
||||||
|
|
||||||
/** Data that resets on switch (stat stages, battler tags, etc.) */
|
/** Data that resets on switch (stat stages, battler tags, etc.) */
|
||||||
public summonData: PokemonSummonData;
|
public summonData: PokemonSummonData = new PokemonSummonData;
|
||||||
/** Wave data correponding to moves/ability information revealed */
|
/** Wave data correponding to moves/ability information revealed */
|
||||||
public waveData: PokemonWaveData = new PokemonWaveData;
|
public waveData: PokemonWaveData = new PokemonWaveData;
|
||||||
/** Data that resets only on battle end (hit count, harvest berries, etc.) */
|
/** Data that resets only on battle end (hit count, harvest berries, etc.) */
|
||||||
@ -354,8 +354,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
private shinySparkle: Phaser.GameObjects.Sprite;
|
private shinySparkle: Phaser.GameObjects.Sprite;
|
||||||
|
|
||||||
// TODO: Rework this constructor - it's _far_ too complicated and could be modernized
|
// TODO: Rework this eventually
|
||||||
// in a similar manner to the PokemonData constructor
|
|
||||||
constructor(
|
constructor(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -237,6 +237,10 @@ export abstract class PersistentModifier extends Modifier {
|
|||||||
|
|
||||||
abstract getMaxStackCount(forThreshold?: boolean): number;
|
abstract getMaxStackCount(forThreshold?: boolean): number;
|
||||||
|
|
||||||
|
getCountUnderMax(): number {
|
||||||
|
return this.getMaxStackCount() - this.getStackCount();
|
||||||
|
}
|
||||||
|
|
||||||
isIconVisible(): boolean {
|
isIconVisible(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -658,7 +662,9 @@ export class TerastallizeAccessModifier extends PersistentModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
||||||
|
/** The ID of the {@linkcode Pokemon} that this item belongs to. */
|
||||||
public pokemonId: number;
|
public pokemonId: number;
|
||||||
|
/** Whether this item can be transfered to or stolen by another Pokemon. */
|
||||||
public isTransferable = true;
|
public isTransferable = true;
|
||||||
|
|
||||||
constructor(type: ModifierType, pokemonId: number, stackCount?: number) {
|
constructor(type: ModifierType, pokemonId: number, stackCount?: number) {
|
||||||
|
@ -188,7 +188,7 @@ export class EncounterPhase extends BattlePhase {
|
|||||||
];
|
];
|
||||||
const moveset: string[] = [];
|
const moveset: string[] = [];
|
||||||
for (const move of enemyPokemon.getMoveset()) {
|
for (const move of enemyPokemon.getMoveset()) {
|
||||||
moveset.push(move!.getName()); // TODO: remove `!` after moveset-null removal PR
|
moveset.push(move.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -5,15 +5,19 @@ import { Nature } from "#enums/nature";
|
|||||||
import type { PokeballType } from "#enums/pokeball";
|
import type { PokeballType } from "#enums/pokeball";
|
||||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species";
|
||||||
import { Status } from "../data/status-effect";
|
import { Status } from "../data/status-effect";
|
||||||
import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData, type PokemonBattleData } from "../field/pokemon";
|
import Pokemon, {
|
||||||
|
EnemyPokemon,
|
||||||
|
type PokemonMove,
|
||||||
|
type PokemonSummonData,
|
||||||
|
type PokemonBattleData,
|
||||||
|
} from "../field/pokemon";
|
||||||
import { TrainerSlot } from "#enums/trainer-slot";
|
import { TrainerSlot } from "#enums/trainer-slot";
|
||||||
import type { Variant } from "#app/sprites/variant";
|
import type { Variant } from "#app/sprites/variant";
|
||||||
import type { Biome } from "#enums/biome";
|
import type { Biome } from "#enums/biome";
|
||||||
import { Moves } from "#enums/moves";
|
import type { Moves } from "#enums/moves";
|
||||||
import type { Species } from "#enums/species";
|
import type { Species } from "#enums/species";
|
||||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||||
import type { PokemonType } from "#enums/pokemon-type";
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
import { loadBattlerTag } from "#app/data/battler-tags";
|
|
||||||
|
|
||||||
export default class PokemonData {
|
export default class PokemonData {
|
||||||
public id: number;
|
public id: number;
|
||||||
@ -80,9 +84,8 @@ export default class PokemonData {
|
|||||||
* Construct a new {@linkcode PokemonData} instance out of a {@linkcode Pokemon}
|
* Construct a new {@linkcode PokemonData} instance out of a {@linkcode Pokemon}
|
||||||
* or JSON representation thereof.
|
* or JSON representation thereof.
|
||||||
* @param source The {@linkcode Pokemon} to convert into data (or a JSON object representing one)
|
* @param source The {@linkcode Pokemon} to convert into data (or a JSON object representing one)
|
||||||
* @param forHistory
|
|
||||||
*/
|
*/
|
||||||
constructor(source: Pokemon | any, forHistory = false) {
|
constructor(source: Pokemon | any) {
|
||||||
const sourcePokemon = source instanceof Pokemon ? source : undefined;
|
const sourcePokemon = source instanceof Pokemon ? source : undefined;
|
||||||
this.id = source.id;
|
this.id = source.id;
|
||||||
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
|
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
|
||||||
@ -129,23 +132,8 @@ export default class PokemonData {
|
|||||||
|
|
||||||
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
|
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
|
||||||
|
|
||||||
// Deprecated, but needed for session data migration
|
this.moveset = sourcePokemon?.moveset ?? source.moveset;
|
||||||
// TODO: Do we really need this??
|
|
||||||
this.natureOverride = source.natureOverride;
|
|
||||||
this.mysteryEncounterPokemonData = source.mysteryEncounterPokemonData
|
|
||||||
? new CustomPokemonData(source.mysteryEncounterPokemonData)
|
|
||||||
: null;
|
|
||||||
this.fusionMysteryEncounterPokemonData = source.fusionMysteryEncounterPokemonData
|
|
||||||
? new CustomPokemonData(source.fusionMysteryEncounterPokemonData)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
this.moveset =
|
|
||||||
sourcePokemon?.moveset ??
|
|
||||||
(source.moveset || [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)])
|
|
||||||
.filter((m: any) => !!m)
|
|
||||||
.map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp, m.virtual, m.maxPpOverride));
|
|
||||||
|
|
||||||
if (!forHistory) {
|
|
||||||
this.levelExp = source.levelExp;
|
this.levelExp = source.levelExp;
|
||||||
this.hp = source.hp;
|
this.hp = source.hp;
|
||||||
|
|
||||||
@ -160,25 +148,13 @@ export default class PokemonData {
|
|||||||
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
// enemy pokemon don't use instantized summon data
|
this.summonData = source.summonData;
|
||||||
if (this.player) {
|
|
||||||
this.summonData = sourcePokemon?.summonData ?? source.summonData;
|
|
||||||
} else {
|
|
||||||
console.log("this.player false!");
|
|
||||||
this.summonData = new PokemonSummonData();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourcePokemon) {
|
|
||||||
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
|
|
||||||
this.summonData.tags = source.tags.map((t: any) => loadBattlerTag(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.summonDataSpeciesFormIndex = sourcePokemon
|
this.summonDataSpeciesFormIndex = sourcePokemon
|
||||||
? this.getSummonDataSpeciesFormIndex()
|
? this.getSummonDataSpeciesFormIndex()
|
||||||
: source.summonDataSpeciesFormIndex;
|
: source.summonDataSpeciesFormIndex;
|
||||||
this.battleData = sourcePokemon?.battleData ?? source.battleData;
|
this.battleData = sourcePokemon?.battleData ?? source.battleData;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toPokemon(battleType?: BattleType, partyMemberIndex = 0, double = false): Pokemon {
|
toPokemon(battleType?: BattleType, partyMemberIndex = 0, double = false): Pokemon {
|
||||||
const species = getPokemonSpecies(this.species);
|
const species = getPokemonSpecies(this.species);
|
||||||
|
@ -59,6 +59,10 @@ import * as v1_7_0 from "./versions/v1_7_0";
|
|||||||
// biome-ignore lint/style/noNamespaceImport: Convenience
|
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||||
import * as v1_8_3 from "./versions/v1_8_3";
|
import * as v1_8_3 from "./versions/v1_8_3";
|
||||||
|
|
||||||
|
// --- v1.9.0 PATCHES --- //
|
||||||
|
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||||
|
import * as v1_9_0 from "./versions/v1_9_0";
|
||||||
|
|
||||||
/** Current game version */
|
/** Current game version */
|
||||||
const LATEST_VERSION = version;
|
const LATEST_VERSION = version;
|
||||||
|
|
||||||
@ -80,6 +84,7 @@ systemMigrators.push(...v1_8_3.systemMigrators);
|
|||||||
const sessionMigrators: SessionSaveMigrator[] = [];
|
const sessionMigrators: SessionSaveMigrator[] = [];
|
||||||
sessionMigrators.push(...v1_0_4.sessionMigrators);
|
sessionMigrators.push(...v1_0_4.sessionMigrators);
|
||||||
sessionMigrators.push(...v1_7_0.sessionMigrators);
|
sessionMigrators.push(...v1_7_0.sessionMigrators);
|
||||||
|
sessionMigrators.push(...v1_9_0.sessionMigrators);
|
||||||
|
|
||||||
/** All settings migrators */
|
/** All settings migrators */
|
||||||
const settingsMigrators: SettingsSaveMigrator[] = [];
|
const settingsMigrators: SettingsSaveMigrator[] = [];
|
||||||
|
36
src/system/version_migration/versions/v1_9_0.ts
Normal file
36
src/system/version_migration/versions/v1_9_0.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
|
||||||
|
import { loadBattlerTag } from "#app/data/battler-tags";
|
||||||
|
import { PokemonMove } from "#app/field/pokemon";
|
||||||
|
import type { SessionSaveData } from "#app/system/game-data";
|
||||||
|
import PokemonData from "#app/system/pokemon-data";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { PokeballType } from "#enums/pokeball";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate all lingering rage fist data inside `CustomPokemonData`,
|
||||||
|
* as well as enforcing default values across the board.
|
||||||
|
* @param data - {@linkcode SystemSaveData}
|
||||||
|
*/
|
||||||
|
const migratePartyData: SessionSaveMigrator = {
|
||||||
|
version: "1.9.0",
|
||||||
|
migrate: (data: SessionSaveData): void => {
|
||||||
|
data.party = data.party.map(pkmnData => {
|
||||||
|
// this stuff is copied straight from the constructor fwiw
|
||||||
|
pkmnData.moveset = pkmnData.moveset.filter(m => !!m) ?? [
|
||||||
|
new PokemonMove(Moves.TACKLE),
|
||||||
|
new PokemonMove(Moves.GROWL),
|
||||||
|
];
|
||||||
|
pkmnData.pokeball ??= PokeballType.POKEBALL;
|
||||||
|
pkmnData.summonData.tags = pkmnData.summonData.tags.map((t: any) => loadBattlerTag(t));
|
||||||
|
if (
|
||||||
|
"hitsRecCount" in pkmnData.customPokemonData &&
|
||||||
|
typeof pkmnData.customPokemonData["hitsRecCount"] === "number"
|
||||||
|
) {
|
||||||
|
pkmnData.battleData.hitCount = pkmnData.customPokemonData?.["hitsRecCount"];
|
||||||
|
}
|
||||||
|
return new PokemonData(pkmnData);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionMigrators: Readonly<SessionSaveMigrator[]> = [migratePartyData] as const;
|
@ -469,7 +469,6 @@ export function truncateString(str: string, maxLength = 10) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a deep copy of an object.
|
* Perform a deep copy of an object.
|
||||||
*
|
|
||||||
* @param values - The object to be deep copied.
|
* @param values - The object to be deep copied.
|
||||||
* @returns A new object that is a deep copy of the input.
|
* @returns A new object that is a deep copy of the input.
|
||||||
*/
|
*/
|
||||||
@ -480,22 +479,20 @@ export function deepCopy(values: object): object {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a space-separated string into a capitalized and underscored string.
|
* Convert a space-separated string into a capitalized and underscored string.
|
||||||
*
|
|
||||||
* @param input - The string to be converted.
|
* @param input - The string to be converted.
|
||||||
* @returns The converted string with words capitalized and separated by underscores.
|
* @returns The converted string with words capitalized and separated by underscores.
|
||||||
*/
|
*/
|
||||||
export function reverseValueToKeySetting(input) {
|
export function reverseValueToKeySetting(input: string) {
|
||||||
// Split the input string into an array of words
|
// Split the input string into an array of words
|
||||||
const words = input.split(" ");
|
const words = input.split(" ");
|
||||||
// Capitalize the first letter of each word and convert the rest to lowercase
|
// Capitalize the first letter of each word and convert the rest to lowercase
|
||||||
const capitalizedWords = words.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
const capitalizedWords = words.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
||||||
// Join the capitalized words with underscores and return the result
|
// Join the capitalized words with underscores and return the result
|
||||||
return capitalizedWords.join("_");
|
return capitalizedWords.join("_");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capitalize a string.
|
* Capitalize a string.
|
||||||
*
|
|
||||||
* @param str - The string to be capitalized.
|
* @param str - The string to be capitalized.
|
||||||
* @param sep - The separator between the words of the string.
|
* @param sep - The separator between the words of the string.
|
||||||
* @param lowerFirstChar - Whether the first character of the string should be lowercase or not.
|
* @param lowerFirstChar - Whether the first character of the string should be lowercase or not.
|
||||||
@ -515,8 +512,8 @@ export function capitalizeString(str: string, sep: string, lowerFirstChar = true
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isNullOrUndefined(object: any): object is undefined | null {
|
export function isNullOrUndefined(object: any): object is null | undefined {
|
||||||
return null === object || undefined === object;
|
return object === null || object === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -579,25 +576,3 @@ export function animationFileName(move: Moves): string {
|
|||||||
export function camelCaseToKebabCase(str: string): string {
|
export function camelCaseToKebabCase(str: string): string {
|
||||||
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
|
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges the two objects, such that for each property in `b` that matches a property in `a`,
|
|
||||||
* the value in `a` is replaced by the value in `b`. This is done recursively if the property is a non-array object
|
|
||||||
*
|
|
||||||
* If the property does not exist in `a` or its `typeof` evaluates differently, the property is skipped.
|
|
||||||
* If the value of the property is an array, the array is replaced. If it is any other object, the object is merged recursively.
|
|
||||||
*/
|
|
||||||
// biome-ignore lint/complexity/noBannedTypes: This function is designed to merge json objects
|
|
||||||
export function deepMergeObjects(a: Object, b: Object) {
|
|
||||||
for (const key in b) {
|
|
||||||
// !(key in a) is redundant here, yet makes it clear that we're explicitly interested in properties that exist in `a`
|
|
||||||
if (!(key in a) || typeof a[key] !== typeof b[key]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (typeof b[key] === "object" && !Array.isArray(b[key])) {
|
|
||||||
deepMergeObjects(a[key], b[key]);
|
|
||||||
} else {
|
|
||||||
a[key] = b[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
33
src/utils/data.ts
Normal file
33
src/utils/data.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Deeply merge two JSON objects' common properties together.
|
||||||
|
* This copies all values from `source` that match properties inside `dest`,
|
||||||
|
* checking recursively for non-null nested objects.
|
||||||
|
|
||||||
|
* If a property in `src` does not exist in `dest` or its `typeof` evaluates differently, it is skipped.
|
||||||
|
* If it is a non-array object, its properties are recursed into and checked in turn.
|
||||||
|
* All other values are copied verbatim.
|
||||||
|
* @param dest The object to merge values into
|
||||||
|
* @param source The object to source merged values from
|
||||||
|
* @remarks Do not use for regular objects; this is specifically made for JSON copying.
|
||||||
|
* @see deepMergeObjects
|
||||||
|
*/
|
||||||
|
export function deepMergeSpriteData(dest: object, source: object) {
|
||||||
|
// Grab all the keys present in both with similar types
|
||||||
|
const matchingKeys = Object.keys(source).filter(key => {
|
||||||
|
const destVal = dest[key];
|
||||||
|
const sourceVal = source[key];
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Somewhat redundant, but makes it clear that we're explicitly interested in properties that exist in both
|
||||||
|
key in source && Array.isArray(sourceVal) === Array.isArray(destVal) && typeof sourceVal === typeof destVal
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const key of matchingKeys) {
|
||||||
|
if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) {
|
||||||
|
deepMergeSpriteData(dest[key], source[key]);
|
||||||
|
} else {
|
||||||
|
dest[key] = source[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -31,7 +31,7 @@ describe("Abilities - Cud Chew", () => {
|
|||||||
.moveset([Moves.BUG_BITE, Moves.SPLASH, Moves.HYPER_VOICE, Moves.STUFF_CHEEKS])
|
.moveset([Moves.BUG_BITE, Moves.SPLASH, Moves.HYPER_VOICE, Moves.STUFF_CHEEKS])
|
||||||
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }])
|
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }])
|
||||||
.ability(Abilities.CUD_CHEW)
|
.ability(Abilities.CUD_CHEW)
|
||||||
.battleType("single")
|
.battleStyle("single")
|
||||||
.disableCrits()
|
.disableCrits()
|
||||||
.enemySpecies(Species.MAGIKARP)
|
.enemySpecies(Species.MAGIKARP)
|
||||||
.enemyAbility(Abilities.BALL_FETCH)
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type Pokemon from "#app/field/pokemon";
|
import type Pokemon from "#app/field/pokemon";
|
||||||
import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier";
|
import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier";
|
||||||
import type { ModifierOverride } from "#app/modifier/modifier-type";
|
import type { ModifierOverride } from "#app/modifier/modifier-type";
|
||||||
import type { BooleanHolder } from "#app/utils";
|
import type { BooleanHolder } from "#app/utils/common";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
import { BerryType } from "#enums/berry-type";
|
import { BerryType } from "#enums/berry-type";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
@ -45,7 +45,7 @@ describe("Abilities - Harvest", () => {
|
|||||||
.moveset([Moves.SPLASH, Moves.NATURAL_GIFT, Moves.FALSE_SWIPE, Moves.GASTRO_ACID])
|
.moveset([Moves.SPLASH, Moves.NATURAL_GIFT, Moves.FALSE_SWIPE, Moves.GASTRO_ACID])
|
||||||
.ability(Abilities.HARVEST)
|
.ability(Abilities.HARVEST)
|
||||||
.startingLevel(100)
|
.startingLevel(100)
|
||||||
.battleType("single")
|
.battleStyle("single")
|
||||||
.disableCrits()
|
.disableCrits()
|
||||||
.statusActivation(false) // Since we're using nuzzle to proc both enigma and sitrus berries
|
.statusActivation(false) // Since we're using nuzzle to proc both enigma and sitrus berries
|
||||||
.weather(WeatherType.SUNNY) // guaranteed recovery
|
.weather(WeatherType.SUNNY) // guaranteed recovery
|
||||||
|
@ -6,6 +6,9 @@ import type BattleScene from "#app/battle-scene";
|
|||||||
import { Moves } from "#app/enums/moves";
|
import { Moves } from "#app/enums/moves";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||||
|
import PokemonData from "#app/system/pokemon-data";
|
||||||
|
import { PlayerPokemon } from "#app/field/pokemon";
|
||||||
|
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||||
|
|
||||||
describe("Spec - Pokemon", () => {
|
describe("Spec - Pokemon", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -209,4 +212,28 @@ describe("Spec - Pokemon", () => {
|
|||||||
expect(types[1]).toBe(PokemonType.DARK);
|
expect(types[1]).toBe(PokemonType.DARK);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should be more or less equivalent when converting to and from PokemonData", async () => {
|
||||||
|
await game.classicMode.startBattle([Species.ALAKAZAM]);
|
||||||
|
const alakazam = game.scene.getPlayerPokemon()!;
|
||||||
|
expect(alakazam).toBeDefined();
|
||||||
|
|
||||||
|
alakazam.hp = 5;
|
||||||
|
const alakaData = new PokemonData(alakazam);
|
||||||
|
const alaka2 = new PlayerPokemon(
|
||||||
|
getPokemonSpecies(Species.ALAKAZAM),
|
||||||
|
5,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
alakaData,
|
||||||
|
);
|
||||||
|
for (const key of Object.keys(alakazam).filter(k => k in alakaData)) {
|
||||||
|
expect(alakazam[key]).toEqual(alaka2["key"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { expect, describe, it, beforeAll } from "vitest";
|
import { expect, describe, it, beforeAll } from "vitest";
|
||||||
import { randomString, padInt } from "#app/utils/common";
|
import { randomString, padInt } from "#app/utils/common";
|
||||||
|
import { deepMergeSpriteData } from "#app/utils/data";
|
||||||
|
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ describe("utils", () => {
|
|||||||
type: Phaser.HEADLESS,
|
type: Phaser.HEADLESS,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("randomString", () => {
|
describe("randomString", () => {
|
||||||
it("should return a string of the specified length", () => {
|
it("should return a string of the specified length", () => {
|
||||||
const str = randomString(10);
|
const str = randomString(10);
|
||||||
@ -46,4 +48,33 @@ describe("utils", () => {
|
|||||||
expect(result).toBe("1");
|
expect(result).toBe("1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("deepMergeSpriteData", () => {
|
||||||
|
it("should merge two objects' common properties", () => {
|
||||||
|
const dest = { a: 1, b: 2 };
|
||||||
|
const source = { a: 3, b: 3, e: 4 };
|
||||||
|
deepMergeSpriteData(dest, source);
|
||||||
|
expect(dest).toEqual({ a: 3, b: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does nothing for identical objects", () => {
|
||||||
|
const dest = { a: 1, b: 2 };
|
||||||
|
const source = { a: 1, b: 2 };
|
||||||
|
deepMergeSpriteData(dest, source);
|
||||||
|
expect(dest).toEqual({ a: 1, b: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve missing and mistyped properties", () => {
|
||||||
|
const dest = { a: 1, c: 56, d: "test" };
|
||||||
|
const source = { a: "apple", b: 3, d: "no hablo español" };
|
||||||
|
deepMergeSpriteData(dest, source);
|
||||||
|
expect(dest).toEqual({ a: 1, c: 56, d: "no hablo español" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should copy arrays verbatim even with mismatches", () => {
|
||||||
|
const dest = { a: 1, b: [{ d: 1 }, { d: 2 }, { d: 3 }] };
|
||||||
|
const source = { a: 3, b: [{ c: [4, 5] }, { p: [7, 8] }], e: 4 };
|
||||||
|
deepMergeSpriteData(dest, source);
|
||||||
|
expect(dest).toEqual({ a: 3, b: [{ c: [4, 5] }, { p: [7, 8] }] });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user