Added version migrator for rage fist data + deepMergeSpriteData tests

This commit is contained in:
Bertie690 2025-04-20 23:16:18 -04:00
parent c3db9e3532
commit 079d50e2e0
16 changed files with 215 additions and 104 deletions

View File

@ -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;

View File

@ -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;
} }
} }
} }

View File

@ -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

View File

@ -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,

View File

@ -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) {

View File

@ -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(

View File

@ -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);

View File

@ -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[] = [];

View 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;

View File

@ -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
View 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];
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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"]);
}
});
}); });

View File

@ -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] }] });
});
});
}); });