Moved empty moveset verification mapping thing to upgrade script bc i wanted to

This commit is contained in:
Bertie690 2025-04-21 12:51:28 -04:00
parent 2746a658df
commit 102554cdb7
10 changed files with 139 additions and 122 deletions

View File

@ -3273,13 +3273,13 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
/**
* Conditionally provides immunity to stat drop effects to the user's field.
*
*
* Used by {@linkcode Abilities.FLOWER_VEIL | Flower Veil}.
*/
export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbAttr {
/** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */
protected protectedStat?: BattleStat;
/** If the method evaluates to true, the stat will be protected. */
protected condition: (target: Pokemon) => boolean;
@ -3296,7 +3296,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA
* @param stat The stat being affected
* @param cancelled Holds whether the stat change was already prevented.
* @param args Args[0] is the target pokemon of the stat change.
* @returns
* @returns
*/
override canApplyPreStatStageChange(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: BooleanHolder, args: [Pokemon, ...any]): boolean {
const target = args[0];
@ -3428,7 +3428,7 @@ export class BonusCritAbAttr extends AbAttr {
/**
* Apply the bonus crit ability by increasing the value in the provided number holder by 1
*
*
* @param pokemon The pokemon with the BonusCrit ability (unused)
* @param passive Unused
* @param simulated Unused
@ -3581,7 +3581,7 @@ export class PreWeatherEffectAbAttr extends AbAttr {
args: any[]): boolean {
return true;
}
applyPreWeatherEffect(
pokemon: Pokemon,
passive: boolean,
@ -4143,25 +4143,24 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
* Used by {@linkcode Abilities.CUD_CHEW}.
*/
export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
constructor() {
super(true);
}
// no need for constructor; all it does is set `showAbility` which we override before triggering anyways
/**
* @returns `true` if the pokemon ate anything last turn
*/
override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
this.showAbility = true; // force ability popup if ability triggers
return !!pokemon.summonData.berriesEatenLast.length;
}
/**
* Cause this {@linkcode Pokemon} to regurgitate and eat all berries
* inside its `berriesEatenLast` array.
* @param pokemon The pokemon having the tummy ache
* @param _passive N/A
* @param _simulated N/A
* @param _cancelled N/A
* @param _args N/A
* @param pokemon - The pokemon having the tummy ache
* @param _passive - N/A
* @param _simulated - N/A
* @param _cancelled - N/A
* @param _args - N/A
*/
override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder | null, _args: any[]): void {
// play berry animation
@ -4182,6 +4181,7 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
* @returns `true` if the pokemon ate anything this turn (we move it into `battleData`)
*/
override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
this.showAbility = false; // don't show popup for turn end berry moving (should ideally be hidden)
return !!pokemon.turnData.berriesEaten.length;
}

View File

@ -2694,7 +2694,9 @@ export class EatBerryAttr extends MoveEffectAttr {
if (!preserve.value) {
this.reduceBerryModifier(target);
}
this.eatBerry(target);
// Don't update harvest for berries preserved via Berry pouch (no item dupes lol)
this.eatBerry(target, undefined, !preserve.value);
return true;
}
@ -2711,15 +2713,21 @@ export class EatBerryAttr extends MoveEffectAttr {
globalScene.updateModifiers(target.isPlayer());
}
eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer) {
// consumer eats berry, owner triggers unburden and similar effects
// These are the same under normal circumstances
/**
* Internal function to apply berry effects.
*
* @param consumer - The {@linkcode Pokemon} eating the berry; assumed to also be owner if `berryOwner` is omitted
* @param berryOwner - The {@linkcode Pokemon} whose berry is being eaten; defaults to `consumer` if not specified.
* @param updateHarvest - Whether to prevent harvest from tracking berries;
* defaults to whether `consumer` equals `berryOwner` (i.e. consuming own berry).
*/
eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) {
// consumer eats berry, owner triggers unburden and similar effects
getBerryEffectFunc(this.chosenBerry.berryType)(consumer);
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner, false);
applyAbAttrs(HealFromBerryUseAbAttr, consumer, new BooleanHolder(false));
// Harvest doesn't track berries eaten by other pokemon
consumer.recordEatenBerry(this.chosenBerry.berryType, berryOwner !== consumer);
consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest);
}
}

View File

@ -279,6 +279,12 @@ export enum FieldPosition {
}
export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
* used to determine various parameters of this Pokemon.
* Represented as a random 32-bit unsigned integer.
* TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon
*/
public id: number;
public name: string;
public nickname: string;
@ -324,7 +330,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public fusionCustomPokemonData: CustomPokemonData | null;
public fusionTeraType: PokemonType;
public customPokemonData: CustomPokemonData = new CustomPokemonData();
public customPokemonData: CustomPokemonData = new CustomPokemonData;
/**
* TODO: Figure out if we can remove this thing
@ -7892,7 +7898,7 @@ export class PokemonSummonData {
public gender: Gender;
public fusionGender: Gender;
public stats: number[] = [0, 0, 0, 0, 0, 0];
public moveset: PokemonMove[];
public moveset: PokemonMove[] | null;
public illusionBroken: boolean = false;
// If not initialized this value will not be populated from save data.
@ -7915,6 +7921,8 @@ export class PokemonSummonData {
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
if (!isNullOrUndefined(source)) {
Object.assign(this, source)
this.moveset &&= this.moveset.map(m => PokemonMove.loadMove(m))
this.tags &&= this.tags.map(t => loadBattlerTag(t))
}
}
}

View File

@ -2,14 +2,14 @@ import { BattleType } from "#enums/battle-type";
import { globalScene } from "#app/global-scene";
import type { Gender } from "../data/gender";
import { Nature } from "#enums/nature";
import type { PokeballType } from "#enums/pokeball";
import { PokeballType } from "#enums/pokeball";
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species";
import type { Status } from "../data/status-effect";
import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonMove, PokemonSummonData } 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";
@ -59,11 +59,11 @@ export default class PokemonData {
public fusionTeraType: PokemonType;
public boss: boolean;
public bossSegments?: number;
public bossSegments: number;
// Effects that need to be preserved between waves
public summonData: PokemonSummonData = new PokemonSummonData();
public battleData: PokemonBattleData = new PokemonBattleData();
public summonData: PokemonSummonData;
public battleData: PokemonBattleData;
public summonDataSpeciesFormIndex: number;
public customPokemonData: CustomPokemonData;
@ -87,7 +87,7 @@ export default class PokemonData {
this.passive = source.passive;
this.shiny = sourcePokemon?.isShiny() ?? source.shiny;
this.variant = sourcePokemon?.getVariant() ?? source.variant;
this.pokeball = source.pokeball;
this.pokeball = source.pokeball ?? PokeballType.POKEBALL;
this.level = source.level;
this.exp = source.exp;
this.levelExp = source.levelExp;
@ -98,7 +98,7 @@ export default class PokemonData {
// TODO: Can't we move some of this verification stuff to an upgrade script?
this.nature = source.nature ?? Nature.HARDY;
this.moveset = source.moveset ?? [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)];
this.moveset = source.moveset.map((m: any) => PokemonMove.loadMove(m));
this.status = source.status ?? null;
this.friendship = source.friendship ?? getPokemonSpecies(this.species).baseFriendship;
this.metLevel = source.metLevel || 5;

View File

@ -1,11 +1,9 @@
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
import { loadBattlerTag } from "#app/data/battler-tags";
import { Status } from "#app/data/status-effect";
import { PokemonMove } from "#app/field/pokemon";
import type { SessionSaveData } from "#app/system/game-data";
import PokemonData from "#app/system/pokemon-data";
import type PokemonData from "#app/system/pokemon-data";
import { Moves } from "#enums/moves";
import { PokeballType } from "#enums/pokeball";
/**
* Migrate all lingering rage fist data inside `CustomPokemonData`,
@ -23,23 +21,26 @@ const migratePartyData: SessionSaveMigrator = {
pkmnData.status.sleepTurnsRemaining,
);
// remove empty moves from moveset
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));
pkmnData.moveset = (pkmnData.moveset ?? [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)]).filter(
m => !!m,
);
// only edit summondata moveset if exists
pkmnData.summonData.moveset &&= pkmnData.summonData.moveset.filter(m => !!m);
if (
pkmnData.customPokemonData &&
"hitsRecCount" in pkmnData.customPokemonData &&
typeof pkmnData.customPokemonData["hitsRecCount"] === "number"
) {
pkmnData.battleData.hitCount = pkmnData.customPokemonData?.["hitsRecCount"];
// transfer old hit count stat to battleData.
// No need to reset it as new Pokemon
pkmnData.battleData.hitCount = pkmnData.customPokemonData["hitsRecCount"];
}
pkmnData = new PokemonData(pkmnData);
return pkmnData;
};
data.party.forEach(mapParty);
data.enemyParty.forEach(mapParty);
data.party = data.party.map(mapParty);
data.enemyParty = data.enemyParty.map(mapParty);
},
};

View File

@ -1,6 +1,7 @@
import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability";
import { getBerryEffectFunc } from "#app/data/berry";
import Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { Abilities } from "#enums/abilities";
import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
@ -65,6 +66,37 @@ describe("Abilities - Cud Chew", () => {
expect(farigiraf.turnData.berriesEaten).toEqual([]);
});
it("shouldn't show ability popup for end-of-turn storage", async () => {
const abDisplaySpy = vi.spyOn(globalScene, "queueAbilityDisplay");
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1; // needed to allow sitrus procs
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
// doesn't trigger since cud chew hasn't eaten berry yet
expect(abDisplaySpy).not.toHaveBeenCalledWith(farigiraf);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
// globalScene.queueAbilityDisplay should be called twice: once to show cud chew before regurgitating berries,
// once to hide after finishing application
expect(abDisplaySpy).toBeCalledTimes(2);
expect(abDisplaySpy.mock.calls[0][0]).toBe(farigiraf);
expect(abDisplaySpy.mock.calls[0][2]).toBe(true);
expect(abDisplaySpy.mock.calls[1][0]).toBe(farigiraf);
expect(abDisplaySpy.mock.calls[1][2]).toBe(false);
await game.phaseInterceptor.to("TurnEndPhase");
// not called again at turn end
expect(abDisplaySpy).toBeCalledTimes(2);
});
it("can store multiple berries across 2 turns with teatime", async () => {
// always eat first berry for stuff cheeks & company
vi.spyOn(Pokemon.prototype, "randSeedInt").mockReturnValue(0);

View File

@ -254,7 +254,7 @@ describe("Abilities - Harvest", () => {
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase", false);
// won;t trigger harvest since we didn't lose the berry (it just doesn't ever add it to the array)
// won't trigger harvest since we didn't lose the berry (it just doesn't ever add it to the array)
expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]);
expectBerriesContaining(...initBerries);
});

View File

@ -6,9 +6,6 @@ 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;
@ -212,29 +209,4 @@ describe("Spec - Pokemon", () => {
expect(types[1]).toBe(PokemonType.DARK);
});
});
// TODO: Remove/rework after save data overhaul
it("should preserve common fields 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

@ -32,6 +32,7 @@ describe("Moves - Rage Fist", () => {
.moveset([Moves.RAGE_FIST, Moves.SPLASH, Moves.SUBSTITUTE, Moves.TIDY_UP])
.startingLevel(100)
.enemyLevel(1)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.DOUBLE_KICK);
@ -39,9 +40,7 @@ describe("Moves - Rage Fist", () => {
});
it("should gain power per hit taken", async () => {
game.override.enemySpecies(Species.MAGIKARP);
await game.classicMode.startBattle([Species.MAGIKARP]);
await game.classicMode.startBattle([Species.FEEBAS]);
game.move.select(Moves.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
@ -51,9 +50,7 @@ describe("Moves - Rage Fist", () => {
});
it("caps at 6 hits taken", async () => {
game.override.enemySpecies(Species.MAGIKARP);
await game.classicMode.startBattle([Species.MAGIKARP]);
await game.classicMode.startBattle([Species.FEEBAS]);
// spam splash against magikarp hitting us 2 times per turn
game.move.select(Moves.SPLASH);
@ -72,10 +69,10 @@ describe("Moves - Rage Fist", () => {
expect(move.calculateBattlePower).toHaveLastReturnedWith(350);
});
it("should not count subsitute hits or confusion damage", async () => {
it("should not count substitute hits or confusion damage", async () => {
game.override.enemySpecies(Species.SHUCKLE).enemyMoveset([Moves.CONFUSE_RAY, Moves.DOUBLE_KICK]);
await game.classicMode.startBattle([Species.MAGIKARP]);
await game.classicMode.startBattle([Species.REGIROCK]);
game.move.select(Moves.SUBSTITUTE);
await game.forceEnemyMove(Moves.DOUBLE_KICK);
@ -92,68 +89,52 @@ describe("Moves - Rage Fist", () => {
await game.toNextTurn();
game.move.select(Moves.RAGE_FIST);
await game.forceEnemyMove(Moves.CONFUSE_RAY);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.move.forceConfusionActivation(true);
await game.forceEnemyMove(Moves.DOUBLE_KICK);
await game.phaseInterceptor.to("BerryPhase");
await game.toNextTurn();
// didn't go up
// didn't go up from hitting ourself
expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(0);
await game.toNextTurn();
game.move.select(Moves.RAGE_FIST);
await game.forceEnemyMove(Moves.DOUBLE_KICK);
await game.move.forceConfusionActivation(false);
await game.toNextTurn();
expect(move.calculateBattlePower).toHaveLastReturnedWith(150);
expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(2);
});
it("should maintain hits recieved between wild waves", async () => {
game.override.enemySpecies(Species.MAGIKARP).startingWave(1);
await game.classicMode.startBattle([Species.MAGIKARP]);
await game.classicMode.startBattle([Species.FEEBAS]);
game.move.select(Moves.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave();
game.move.select(Moves.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(2);
expect(move.calculateBattlePower).toHaveLastReturnedWith(250);
expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(4);
});
it("should reset hits recieved before trainer battles", async () => {
game.override.enemySpecies(Species.MAGIKARP).moveset(Moves.DOUBLE_IRON_BASH);
await game.classicMode.startBattle([Species.MARSHADOW]);
const marshadow = game.scene.getPlayerPokemon()!;
expect(marshadow).toBeDefined();
// beat up a magikarp
game.move.select(Moves.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerPokemon()?.battleData.hitCount).toBe(4);
expect(move.calculateBattlePower).toHaveLastReturnedWith(250);
});
it("should reset hits recieved before trainer battles", async () => {
await game.classicMode.startBattle([Species.IRON_HANDS]);
const ironHands = game.scene.getPlayerPokemon()!;
expect(ironHands).toBeDefined();
// beat up a magikarp
game.move.select(Moves.RAGE_FIST);
await game.forceEnemyMove(Moves.DOUBLE_KICK);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.isVictory()).toBe(true);
expect(marshadow.battleData.hitCount).toBe(2);
expect(ironHands.battleData.hitCount).toBe(2);
expect(move.calculateBattlePower).toHaveLastReturnedWith(150);
game.override.battleType(BattleType.TRAINER);
await game.toNextWave();
expect(game.scene.lastEnemyTrainer).not.toBeNull();
expect(marshadow.battleData.hitCount).toBe(0);
game.move.select(Moves.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(move.calculateBattlePower).toHaveLastReturnedWith(150);
expect(ironHands.battleData.hitCount).toBe(0);
});
it("should reset the hitRecCounter if we enter new biome", async () => {
@ -173,24 +154,39 @@ describe("Moves - Rage Fist", () => {
});
it("should not reset the hitRecCounter if switched out", async () => {
game.override.enemySpecies(Species.MAGIKARP).startingWave(1).enemyMoveset(Moves.TACKLE);
game.override.enemyMoveset(Moves.TACKLE);
const getPartyHitCount = () =>
game.scene
.getPlayerParty()
.filter(p => !!p)
.map(m => m.battleData.hitCount);
await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
// Charizard hit
game.move.select(Moves.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
expect(getPartyHitCount()).toEqual([1, 0]);
// blastoise switched in & hit
game.doSwitchPokemon(1);
await game.toNextTurn();
expect(getPartyHitCount()).toEqual([1, 1]);
// charizard switched in & hit
game.doSwitchPokemon(1);
await game.toNextTurn();
expect(getPartyHitCount()).toEqual([2, 1]);
// Charizard rage fist
game.move.select(Moves.RAGE_FIST);
await game.phaseInterceptor.to("MoveEndPhase");
expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(Species.CHARIZARD);
const charizard = game.scene.getPlayerPokemon()!;
expect(charizard).toBeDefined();
expect(charizard.species.speciesId).toBe(Species.CHARIZARD);
expect(move.calculateBattlePower).toHaveLastReturnedWith(150);
});
});

View File

@ -576,7 +576,7 @@ export default class GameManager {
* Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value.
* Used to manually modify Pokemon turn order.
* Note: This *DOES NOT* account for priority, only speed.
* @param {BattlerIndex[]} order The turn order to set
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es.
* @example
* ```ts
* await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]);