mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 09:02:47 +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 {
|
||||
fixedInt,
|
||||
deepMergeObjects,
|
||||
getIvsFromId,
|
||||
randSeedInt,
|
||||
getEnumValues,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
BooleanHolder,
|
||||
type Constructor,
|
||||
} from "#app/utils/common";
|
||||
import { deepMergeSpriteData } from "#app/utils/data";
|
||||
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
||||
import {
|
||||
ConsumableModifier,
|
||||
@ -788,7 +788,7 @@ export default class BattleScene extends SceneBase {
|
||||
return;
|
||||
}
|
||||
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> {
|
||||
@ -836,6 +836,7 @@ export default class BattleScene extends SceneBase {
|
||||
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.
|
||||
* 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}
|
||||
* and {@linkcode EnemyPokemon.isActive is active}
|
||||
* (aka {@linkcode EnemyPokemon.isAllowedInBattle is allowed in battle}),
|
||||
* @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField | on the field}
|
||||
* and {@linkcode EnemyPokemon.isActive | is active}
|
||||
* (aka {@linkcode EnemyPokemon.isAllowedInBattle | is allowed in battle}),
|
||||
* or `undefined` if there are no valid pokemon
|
||||
* @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);
|
||||
}
|
||||
|
||||
// TODO: ...this never actually returns `null`, right?
|
||||
newBattle(
|
||||
waveIndex?: number,
|
||||
battleType?: BattleType,
|
||||
trainerData?: TrainerData,
|
||||
double?: boolean,
|
||||
mysteryEncounterType?: MysteryEncounterType,
|
||||
): Battle | null {
|
||||
): Battle {
|
||||
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
|
||||
const newWaveIndex = waveIndex || (this.currentBattle?.waveIndex || _startingWave - 1) + 1;
|
||||
let newDouble: boolean | undefined;
|
||||
@ -1492,7 +1492,7 @@ export default class BattleScene extends SceneBase {
|
||||
});
|
||||
|
||||
for (const pokemon of this.getPlayerParty()) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetBattleAndWaveData();
|
||||
pokemon.resetTera();
|
||||
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
||||
if (
|
||||
@ -3261,6 +3261,7 @@ export default class BattleScene extends SceneBase {
|
||||
[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 {
|
||||
const modifiers = player ? this.modifiers : (this.enemyModifiers as PersistentModifier[]);
|
||||
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.
|
||||
* If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead.
|
||||
* @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`.
|
||||
* @returns `true` if the item exists and was successfully removed, `false` otherwise.
|
||||
* @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
|
||||
*/
|
||||
removeModifier(modifier: PersistentModifier, enemy = false): boolean {
|
||||
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
|
||||
|
@ -8,15 +8,19 @@ import type { Nature } from "#enums/nature";
|
||||
* Includes abilities, nature, changed types, etc.
|
||||
*/
|
||||
export class CustomPokemonData {
|
||||
public spriteScale = -1;
|
||||
public ability: Abilities | -1 = -1;
|
||||
public passive: Abilities | -1 = -1;
|
||||
public nature: Nature | -1 = -1;
|
||||
public types: PokemonType[] = [];
|
||||
public spriteScale: number;
|
||||
public ability: Abilities | -1;
|
||||
public passive: Abilities | -1;
|
||||
public nature: Nature | -1;
|
||||
public types: PokemonType[];
|
||||
|
||||
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
|
||||
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 { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite";
|
||||
import { hasExpSprite } from "#app/sprites/sprite-utils";
|
||||
import { Gender } from "./gender";
|
||||
|
||||
export enum Region {
|
||||
NORMAL,
|
||||
@ -846,6 +847,23 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
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)
|
||||
* @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 */
|
||||
|
||||
/** 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 */
|
||||
public waveData: PokemonWaveData = new PokemonWaveData;
|
||||
/** 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;
|
||||
|
||||
// TODO: Rework this constructor - it's _far_ too complicated and could be modernized
|
||||
// in a similar manner to the PokemonData constructor
|
||||
// TODO: Rework this eventually
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
|
@ -237,6 +237,10 @@ export abstract class PersistentModifier extends Modifier {
|
||||
|
||||
abstract getMaxStackCount(forThreshold?: boolean): number;
|
||||
|
||||
getCountUnderMax(): number {
|
||||
return this.getMaxStackCount() - this.getStackCount();
|
||||
}
|
||||
|
||||
isIconVisible(): boolean {
|
||||
return true;
|
||||
}
|
||||
@ -658,7 +662,9 @@ export class TerastallizeAccessModifier extends PersistentModifier {
|
||||
}
|
||||
|
||||
export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
||||
/** The ID of the {@linkcode Pokemon} that this item belongs to. */
|
||||
public pokemonId: number;
|
||||
/** Whether this item can be transfered to or stolen by another Pokemon. */
|
||||
public isTransferable = true;
|
||||
|
||||
constructor(type: ModifierType, pokemonId: number, stackCount?: number) {
|
||||
|
@ -188,7 +188,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
];
|
||||
const moveset: string[] = [];
|
||||
for (const move of enemyPokemon.getMoveset()) {
|
||||
moveset.push(move!.getName()); // TODO: remove `!` after moveset-null removal PR
|
||||
moveset.push(move.getName());
|
||||
}
|
||||
|
||||
console.log(
|
||||
@ -550,7 +550,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
if (enemyPokemon.isShiny(true)) {
|
||||
globalScene.unshiftPhase(new ShinySparklePhase(BattlerIndex.ENEMY + e));
|
||||
}
|
||||
/** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */
|
||||
/** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */
|
||||
if (
|
||||
enemyPokemon.species.speciesId === Species.ETERNATUS &&
|
||||
(globalScene.gameMode.isBattleClassicFinalBoss(globalScene.currentBattle.waveIndex) ||
|
||||
|
@ -5,15 +5,19 @@ import { Nature } from "#enums/nature";
|
||||
import type { PokeballType } from "#enums/pokeball";
|
||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species";
|
||||
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 type { Variant } from "#app/sprites/variant";
|
||||
import type { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import type { Moves } from "#enums/moves";
|
||||
import type { Species } from "#enums/species";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import { loadBattlerTag } from "#app/data/battler-tags";
|
||||
|
||||
export default class PokemonData {
|
||||
public id: number;
|
||||
@ -80,9 +84,8 @@ export default class PokemonData {
|
||||
* Construct a new {@linkcode PokemonData} instance out of a {@linkcode Pokemon}
|
||||
* or JSON representation thereof.
|
||||
* @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;
|
||||
this.id = source.id;
|
||||
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
|
||||
@ -129,55 +132,28 @@ export default class PokemonData {
|
||||
|
||||
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
|
||||
|
||||
// Deprecated, but needed for session data migration
|
||||
// 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;
|
||||
|
||||
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));
|
||||
this.levelExp = source.levelExp;
|
||||
this.hp = source.hp;
|
||||
|
||||
if (!forHistory) {
|
||||
this.levelExp = source.levelExp;
|
||||
this.hp = source.hp;
|
||||
this.pauseEvolutions = !!source.pauseEvolutions;
|
||||
this.evoCounter = source.evoCounter ?? 0;
|
||||
|
||||
this.pauseEvolutions = !!source.pauseEvolutions;
|
||||
this.evoCounter = source.evoCounter ?? 0;
|
||||
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
|
||||
this.bossSegments = source.bossSegments;
|
||||
this.status =
|
||||
sourcePokemon?.status ??
|
||||
(source.status
|
||||
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
||||
: null);
|
||||
|
||||
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
|
||||
this.bossSegments = source.bossSegments;
|
||||
this.status =
|
||||
sourcePokemon?.status ??
|
||||
(source.status
|
||||
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
||||
: null);
|
||||
this.summonData = source.summonData;
|
||||
|
||||
// enemy pokemon don't use instantized summon data
|
||||
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.getSummonDataSpeciesFormIndex()
|
||||
: source.summonDataSpeciesFormIndex;
|
||||
this.battleData = sourcePokemon?.battleData ?? source.battleData;
|
||||
}
|
||||
this.summonDataSpeciesFormIndex = sourcePokemon
|
||||
? this.getSummonDataSpeciesFormIndex()
|
||||
: source.summonDataSpeciesFormIndex;
|
||||
this.battleData = sourcePokemon?.battleData ?? source.battleData;
|
||||
}
|
||||
|
||||
toPokemon(battleType?: BattleType, partyMemberIndex = 0, double = false): Pokemon {
|
||||
|
@ -59,6 +59,10 @@ import * as v1_7_0 from "./versions/v1_7_0";
|
||||
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||
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 */
|
||||
const LATEST_VERSION = version;
|
||||
|
||||
@ -80,6 +84,7 @@ systemMigrators.push(...v1_8_3.systemMigrators);
|
||||
const sessionMigrators: SessionSaveMigrator[] = [];
|
||||
sessionMigrators.push(...v1_0_4.sessionMigrators);
|
||||
sessionMigrators.push(...v1_7_0.sessionMigrators);
|
||||
sessionMigrators.push(...v1_9_0.sessionMigrators);
|
||||
|
||||
/** All settings migrators */
|
||||
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;
|
@ -102,9 +102,9 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
// Prevent overlapping overrides on action modification
|
||||
this.submitAction = originalRegistrationAction;
|
||||
this.sanitizeInputs();
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
const onFail = error => {
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.playError();
|
||||
const errorMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.errorMessageFontSize;
|
||||
if (errorMessageFontSize) {
|
||||
|
@ -469,7 +469,6 @@ export function truncateString(str: string, maxLength = 10) {
|
||||
|
||||
/**
|
||||
* Perform a deep copy of an object.
|
||||
*
|
||||
* @param values - The object to be deep copied.
|
||||
* @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.
|
||||
*
|
||||
* @param input - The string to be converted.
|
||||
* @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
|
||||
const words = input.split(" ");
|
||||
// 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
|
||||
return capitalizedWords.join("_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize a string.
|
||||
*
|
||||
* @param str - The string to be capitalized.
|
||||
* @param sep - The separator between the words of the string.
|
||||
* @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;
|
||||
}
|
||||
|
||||
export function isNullOrUndefined(object: any): object is undefined | null {
|
||||
return null === object || undefined === object;
|
||||
export function isNullOrUndefined(object: any): object is null | undefined {
|
||||
return object === null || object === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -579,25 +576,3 @@ export function animationFileName(move: Moves): string {
|
||||
export function camelCaseToKebabCase(str: string): string {
|
||||
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])
|
||||
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }])
|
||||
.ability(Abilities.CUD_CHEW)
|
||||
.battleType("single")
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { BerryModifier, PreserveBerryModifier } from "#app/modifier/modifier";
|
||||
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 { BerryType } from "#enums/berry-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -45,7 +45,7 @@ describe("Abilities - Harvest", () => {
|
||||
.moveset([Moves.SPLASH, Moves.NATURAL_GIFT, Moves.FALSE_SWIPE, Moves.GASTRO_ACID])
|
||||
.ability(Abilities.HARVEST)
|
||||
.startingLevel(100)
|
||||
.battleType("single")
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.statusActivation(false) // Since we're using nuzzle to proc both enigma and sitrus berries
|
||||
.weather(WeatherType.SUNNY) // guaranteed recovery
|
||||
|
@ -6,6 +6,9 @@ import type BattleScene from "#app/battle-scene";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
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", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -209,4 +212,28 @@ describe("Spec - Pokemon", () => {
|
||||
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 { randomString, padInt } from "#app/utils/common";
|
||||
import { deepMergeSpriteData } from "#app/utils/data";
|
||||
|
||||
import Phaser from "phaser";
|
||||
|
||||
@ -9,6 +10,7 @@ describe("utils", () => {
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
describe("randomString", () => {
|
||||
it("should return a string of the specified length", () => {
|
||||
const str = randomString(10);
|
||||
@ -46,4 +48,33 @@ describe("utils", () => {
|
||||
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