Merge branch 'beta' into fix-challenge-filters-again

This commit is contained in:
NightKev 2025-03-23 19:36:49 -07:00 committed by GitHub
commit 1969c745c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 503 additions and 91 deletions

@ -1 +1 @@
Subproject commit 6b3f37cb351552721232f4dabefa17bddb5b9004
Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b

View File

@ -1064,7 +1064,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr {
}
override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void {
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio));
}
@ -3792,7 +3792,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr {
if (!simulated) {
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
globalScene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }));
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER);
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), { result: HitResult.INDIRECT });
}
}
}
@ -4084,7 +4084,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
for (const opp of pokemon.getOpponents()) {
if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !opp.switchOutStatus) {
if (!simulated) {
opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), HitResult.OTHER);
opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT });
globalScene.queueMessage(i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) }));
}
}
@ -4567,7 +4567,7 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr {
override applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): void {
if (!simulated) {
attacker!.damageAndUpdate(Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker!.damageAndUpdate(Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT });
attacker!.turnData.damageTaken += Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio));
}
}
@ -4588,7 +4588,7 @@ export class PostFaintHPDamageAbAttr extends PostFaintAbAttr {
override applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): void {
if (move !== undefined && attacker !== undefined && !simulated) { //If the mon didn't die to indirect damage
const damage = pokemon.turnData.attacksReceived[0].damage;
attacker.damageAndUpdate((damage), HitResult.OTHER);
attacker.damageAndUpdate((damage), { result: HitResult.INDIRECT });
attacker.turnData.damageTaken += damage;
}
}
@ -4989,7 +4989,7 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
(args[0] as Utils.NumberHolder).value = this.multiplier;
pokemon.removeTag(this.tagType);
if (this.recoilDamageFunc) {
pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), HitResult.OTHER, false, false, true, true);
pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), { result: HitResult.INDIRECT, ignoreSegments: true, ignoreFaintPhase: true });
}
}
}

View File

@ -788,7 +788,7 @@ class SpikesTag extends ArenaTrapTag {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
pokemon.damageAndUpdate(damage, HitResult.OTHER);
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
if (pokemon.turnData) {
pokemon.turnData.damageTaken += damage;
}
@ -982,7 +982,7 @@ class StealthRockTag extends ArenaTrapTag {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
pokemon.damageAndUpdate(damage, HitResult.OTHER);
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
if (pokemon.turnData) {
pokemon.turnData.damageTaken += damage;
}
@ -1327,7 +1327,7 @@ class FireGrassPledgeTag extends ArenaTag {
globalScene.unshiftPhase(
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM),
);
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
});
return super.lapse(arena);

View File

@ -757,7 +757,7 @@ export class ConfusedTag extends BattlerTag {
((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100),
);
globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage);
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
pokemon.battleData.hitCount++;
(globalScene.getCurrentPhase() as MovePhase).cancel();
}
@ -818,7 +818,7 @@ export class DestinyBondTag extends BattlerTag {
pokemonNameWithAffix2: getPokemonNameWithAffix(pokemon),
}),
);
pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, false, true);
pokemon.damageAndUpdate(pokemon.hp, { result: HitResult.INDIRECT_KO, ignoreSegments: true });
return false;
}
}
@ -952,7 +952,7 @@ export class SeedTag extends BattlerTag {
new CommonAnimPhase(source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED),
);
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false);
globalScene.unshiftPhase(
new PokemonHealPhase(
@ -1029,7 +1029,7 @@ export class PowderTag extends BattlerTag {
const cancelDamage = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage);
if (!cancelDamage.value) {
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), HitResult.OTHER);
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
}
// "When the flame touched the powder\non the Pokémon, it exploded!"
@ -1082,7 +1082,7 @@ export class NightmareTag extends BattlerTag {
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
}
}
@ -1440,7 +1440,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
}
}
@ -1644,7 +1644,7 @@ export class ContactDamageProtectedTag extends ProtectedTag {
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon();
if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT });
}
}
}
@ -1810,7 +1810,7 @@ export class PerishSongTag extends BattlerTag {
}),
);
} else {
pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, true, true);
pokemon.damageAndUpdate(pokemon.hp, { result: HitResult.INDIRECT_KO, ignoreSegments: true });
}
return ret;
@ -2240,7 +2240,7 @@ export class SaltCuredTag extends BattlerTag {
if (!cancelled.value) {
const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER);
pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8));
pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
globalScene.queueMessage(
i18next.t("battlerTags:saltCuredLapse", {
@ -2288,7 +2288,7 @@ export class CursedTag extends BattlerTag {
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
globalScene.queueMessage(
i18next.t("battlerTags:cursedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -2611,7 +2611,7 @@ export class GulpMissileTag extends BattlerTag {
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
if (!cancelled.value) {
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER);
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT });
}
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {

View File

@ -16,6 +16,7 @@ import type { AttackMoveResult, TurnMove } from "../../field/pokemon";
import type Pokemon from "../../field/pokemon";
import {
EnemyPokemon,
FieldPosition,
HitResult,
MoveResult,
PlayerPokemon,
@ -1646,7 +1647,7 @@ export class RecoilAttr extends MoveEffectAttr {
return false;
}
user.damageAndUpdate(recoilDamage, HitResult.OTHER, false, true, true);
user.damageAndUpdate(recoilDamage, { result: HitResult.INDIRECT, ignoreSegments: true });
globalScene.queueMessage(i18next.t("moveTriggers:hitWithRecoil", { pokemonName: getPokemonNameWithAffix(user) }));
user.turnData.damageTaken += recoilDamage;
@ -1678,7 +1679,7 @@ export class SacrificialAttr extends MoveEffectAttr {
* @returns true if the function succeeds
**/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true);
user.damageAndUpdate(user.hp, { result: HitResult.INDIRECT, ignoreSegments: true });
user.turnData.damageTaken += user.hp;
return true;
@ -1716,7 +1717,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
return false;
}
user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true);
user.damageAndUpdate(user.hp, { result: HitResult.INDIRECT, ignoreSegments: true });
user.turnData.damageTaken += user.hp;
return true;
@ -1758,7 +1759,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
// Check to see if the Pokemon has an ability that blocks non-direct damage
applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled);
if (!cancelled.value) {
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), HitResult.OTHER, false, true, true);
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true });
globalScene.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message
}
return true;
@ -1805,7 +1806,7 @@ export class AddSubstituteAttr extends MoveEffectAttr {
}
const damageTaken = this.roundUp ? Math.ceil(user.getMaxHp() * this.hpCost) : Math.floor(user.getMaxHp() * this.hpCost);
user.damageAndUpdate(damageTaken, HitResult.OTHER, false, true, true);
user.damageAndUpdate(damageTaken, { result: HitResult.INDIRECT, ignoreSegments: true, ignoreFaintPhase: true });
user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id);
return true;
}
@ -1955,7 +1956,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
return false;
}
targetAlly.damageAndUpdate(Math.max(1, Math.floor(1 / 16 * targetAlly.getMaxHp())), HitResult.OTHER);
targetAlly.damageAndUpdate(Math.max(1, Math.floor(1 / 16 * targetAlly.getMaxHp())), { result: HitResult.INDIRECT });
return true;
}
@ -3434,9 +3435,8 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
this.cutRatio = cutRatio;
this.messageCallback = messageCallback;
}
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true);
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), { result: HitResult.INDIRECT });
user.updateInfo();
const ret = super.apply(user, target, move, args);
if (this.messageCallback) {
@ -5328,7 +5328,7 @@ const crashDamageFunc = (user: Pokemon, move: Move) => {
return false;
}
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), HitResult.OTHER, false, true);
user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT });
globalScene.queueMessage(i18next.t("moveTriggers:keptGoingAndCrashed", { pokemonName: getPokemonNameWithAffix(user) }));
user.turnData.damageTaken += Utils.toDmgValue(user.getMaxHp() / 2);
@ -5649,7 +5649,7 @@ export class CurseAttr extends MoveEffectAttr {
return false;
}
const curseRecoilDamage = Math.max(1, Math.floor(user.getMaxHp() / 2));
user.damageAndUpdate(curseRecoilDamage, HitResult.OTHER, false, true, true);
user.damageAndUpdate(curseRecoilDamage, { result: HitResult.INDIRECT, ignoreSegments: true });
globalScene.queueMessage(
i18next.t("battlerTags:cursedOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
@ -6158,9 +6158,16 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) {
const allyPokemon = user.getAlly();
if (slotIndex <= 1) {
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false));
} else if (allyPokemon.isFainted()) {
// Handle cases where revived pokemon needs to get switched in on same turn
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
globalScene.tryRemovePhase((phase: SwitchSummonPhase) => phase instanceof SwitchSummonPhase && phase.getPokemon() === pokemon);
// If the pokemon being revived was alive earlier in the turn, cancel its move
// (revived pokemon can't move in the turn they're brought back)
globalScene.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
if (user.fieldPosition === FieldPosition.CENTER) {
user.setFieldPosition(FieldPosition.LEFT);
}
globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false));
}
}

View File

@ -46,6 +46,7 @@ export default class DamageNumberHandler {
case HitResult.NOT_VERY_EFFECTIVE:
[textColor, shadowColor] = ["#f08030", "#c03028"];
break;
case HitResult.INDIRECT_KO:
case HitResult.ONE_HIT_KO:
[textColor, shadowColor] = ["#a040a0", "#483850"];
break;

View File

@ -4465,11 +4465,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return result;
}
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
const grudgeTag = this.getTag(BattlerTagType.GRUDGE);
const isOneHitKo = result === HitResult.ONE_HIT_KO;
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
const grudgeTag = this.getTag(BattlerTagType.GRUDGE);
if (dmg) {
this.lapseTags(BattlerTagLapseType.HIT);
@ -4484,19 +4482,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.applyModifiers(EnemyEndureChanceModifier, false, this);
}
/**
* We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase.
*/
const damage = this.damageAndUpdate(
isBlockedBySubstitute ? 0 : dmg,
result as DamageResult,
isCritical,
isOneHitKo,
isOneHitKo,
true,
source,
);
/**
* We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase.
*/
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg,
{
result: result as DamageResult,
isCritical,
ignoreFaintPhase: true,
source
});
if (damage > 0) {
if (source.isPlayer()) {
@ -4557,7 +4553,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.unshiftPhase(
new FaintPhase(
this.getBattlerIndex(),
isOneHitKo,
false,
destinyTag,
grudgeTag,
source,
@ -4635,28 +4631,37 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc.
* Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly
* @param damage integer - passed to damage()
* @param result an enum if it's super effective, not very, etc.
* @param critical boolean if move is a critical hit
* @param isCritical boolean if move is a critical hit
* @param ignoreSegments boolean, passed to damage() and not used currently
* @param preventEndure boolean, ignore endure properties of pokemon, passed to damage()
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
* @returns integer of damage done
*/
damageAndUpdate(
damage: number,
result?: DamageResult,
critical = false,
ignoreSegments = false,
preventEndure = false,
ignoreFaintPhase = false,
source?: Pokemon,
damageAndUpdate(damage: number,
{
result = HitResult.EFFECTIVE,
isCritical = false,
ignoreSegments = false,
ignoreFaintPhase = false,
source = undefined,
}:
{
result?: DamageResult,
isCritical?: boolean,
ignoreSegments?: boolean,
ignoreFaintPhase?: boolean,
source?: Pokemon,
} = {}
): number {
const isIndirectDamage = [ HitResult.INDIRECT, HitResult.INDIRECT_KO ].includes(result);
const damagePhase = new DamageAnimPhase(
this.getBattlerIndex(),
damage,
result as DamageResult,
critical,
isCritical
);
globalScene.unshiftPhase(damagePhase);
if (this.switchOutStatus && source) {
@ -4665,7 +4670,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
damage = this.damage(
damage,
ignoreSegments,
preventEndure,
isIndirectDamage,
ignoreFaintPhase,
);
// Damage amount may have changed, but needed to be queued before calling damage function
@ -5575,7 +5580,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
this.resetBattleSummonData();
if (this.summonDataPrimer) {
for (const k of Object.keys(this.summonData)) {
for (const k of Object.keys(this.summonDataPrimer)) {
if (this.summonDataPrimer[k]) {
this.summonData[k] = this.summonDataPrimer[k];
}
@ -7711,8 +7716,10 @@ export enum HitResult {
HEAL,
FAIL,
MISS,
OTHER,
INDIRECT,
IMMUNE,
CONFUSION,
INDIRECT_KO,
}
export type DamageResult =
@ -7720,7 +7727,9 @@ export type DamageResult =
| HitResult.SUPER_EFFECTIVE
| HitResult.NOT_VERY_EFFECTIVE
| HitResult.ONE_HIT_KO
| HitResult.OTHER;
| HitResult.CONFUSION
| HitResult.INDIRECT_KO
| HitResult.INDIRECT;
/** Interface containing the results of a damage calculation for a given move */
export interface DamageCalculationResult {

View File

@ -5,8 +5,7 @@ import { allMoves } from "#app/data/moves/move";
import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
import { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectHealText } from "#app/data/status-effect";
import type { PlayerPokemon } from "#app/field/pokemon";
import Pokemon from "#app/field/pokemon";
import Pokemon, { type PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { EvolutionPhase } from "#app/phases/evolution-phase";

View File

@ -1,4 +1,4 @@
import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability";
import { applyAbAttrs, applyPreLeaveFieldAbAttrs, PreLeaveFieldAbAttr, RunSuccessAbAttr } from "#app/data/ability";
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
@ -29,6 +29,8 @@ export class AttemptRunPhase extends PokemonPhase {
applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance);
if (playerPokemon.randSeedInt(100) < escapeChance.value && !this.forceFailEscape) {
enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs(PreLeaveFieldAbAttr, enemyPokemon));
globalScene.playSound("se/flee");
globalScene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);

View File

@ -21,7 +21,7 @@ export class DamageAnimPhase extends PokemonPhase {
start() {
super.start();
if (this.damageResult === HitResult.ONE_HIT_KO) {
if (this.damageResult === HitResult.ONE_HIT_KO || this.damageResult === HitResult.INDIRECT_KO) {
if (globalScene.moveAnimations) {
globalScene.toggleInvert(true);
}
@ -42,9 +42,11 @@ export class DamageAnimPhase extends PokemonPhase {
applyDamage() {
switch (this.damageResult) {
case HitResult.EFFECTIVE:
case HitResult.CONFUSION:
globalScene.playSound("se/hit");
break;
case HitResult.SUPER_EFFECTIVE:
case HitResult.INDIRECT_KO:
case HitResult.ONE_HIT_KO:
globalScene.playSound("se/hit_strong");
break;
@ -57,7 +59,7 @@ export class DamageAnimPhase extends PokemonPhase {
globalScene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical);
}
if (this.damageResult !== HitResult.OTHER && this.amount > 0) {
if (this.damageResult !== HitResult.INDIRECT && this.amount > 0) {
const flashTimer = globalScene.time.addEvent({
delay: 100,
repeat: 5,

View File

@ -258,7 +258,7 @@ export class FaintPhase extends PokemonPhase {
} else {
// Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase
enemy.hp++;
globalScene.unshiftPhase(new DamageAnimPhase(enemy.getBattlerIndex(), 0, HitResult.OTHER));
globalScene.unshiftPhase(new DamageAnimPhase(enemy.getBattlerIndex(), 0, HitResult.INDIRECT));
this.end();
}
return true;

View File

@ -3,7 +3,6 @@ import type { BattlerIndex } from "#app/battle";
import { CommonAnim } from "#app/data/battle-anims";
import { getStatusEffectHealText } from "#app/data/status-effect";
import { StatusEffect } from "#app/enums/status-effect";
import type { DamageResult } from "#app/field/pokemon";
import { HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { HealingBoosterModifier } from "#app/modifier/modifier";
@ -79,7 +78,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
}
const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value));
if (healAmount.value < 0) {
pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult);
pokemon.damageAndUpdate(healAmount.value * -1, { result: HitResult.INDIRECT });
healAmount.value = 0;
}
// Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock)

View File

@ -195,6 +195,10 @@ export class SummonPhase extends PartyMemberPokemonPhase {
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
pokemon.getSprite().clearTint();
pokemon.resetSummonData();
// necessary to stay transformed during wild waves
if (pokemon.summonData?.speciesForm) {
pokemon.loadAssets(false);
}
globalScene.time.delayedCall(1000, () => this.end());
},
});

View File

@ -66,7 +66,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
const damage = Utils.toDmgValue(pokemon.getMaxHp() / 16);
globalScene.queueMessage(getWeatherDamageMessage(this.weather?.weatherType!, pokemon)!); // TODO: are those bangs correct?
pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true);
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT, ignoreSegments: true });
};
this.executeForAll((pokemon: Pokemon) => {

View File

@ -3,7 +3,7 @@ import { globalScene } from "#app/global-scene";
import type { Gender } from "../data/gender";
import type { Nature } from "#enums/nature";
import type { PokeballType } from "#enums/pokeball";
import { getPokemonSpecies } from "../data/pokemon-species";
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species";
import { Status } from "../data/status-effect";
import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData } from "../field/pokemon";
import { TrainerSlot } from "../data/trainer-config";
@ -14,6 +14,7 @@ import { 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 { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms";
export default class PokemonData {
public id: number;
@ -63,6 +64,7 @@ export default class PokemonData {
public bossSegments?: number;
public summonData: PokemonSummonData;
public summonDataSpeciesFormIndex: number;
/** Data that can customize a Pokemon in non-standard ways from its Species */
public customPokemonData: CustomPokemonData;
@ -145,8 +147,9 @@ export default class PokemonData {
this.moveset = sourcePokemon.moveset;
if (!forHistory) {
this.status = sourcePokemon.status;
if (this.player) {
if (this.player && sourcePokemon.summonData) {
this.summonData = sourcePokemon.summonData;
this.summonDataSpeciesFormIndex = this.getSummonDataSpeciesFormIndex();
}
}
} else {
@ -170,6 +173,8 @@ export default class PokemonData {
this.summonData.ability = source.summonData.ability;
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
this.summonData.types = source.summonData.types;
this.summonData.speciesForm = source.summonData.speciesForm;
this.summonDataSpeciesFormIndex = source.summonDataSpeciesFormIndex;
if (source.summonData.tags) {
this.summonData.tags = source.summonData.tags?.map(t => loadBattlerTag(t));
@ -213,8 +218,28 @@ export default class PokemonData {
this,
);
if (this.summonData) {
// when loading from saved session, recover summonData.speciesFrom and form index species object
// used to stay transformed on reload session
if (this.summonData.speciesForm) {
this.summonData.speciesForm = getPokemonSpeciesForm(
this.summonData.speciesForm.speciesId,
this.summonDataSpeciesFormIndex,
);
}
ret.primeSummonData(this.summonData);
}
return ret;
}
/**
* Method to save summon data species form index
* Necessary in case the pokemon is transformed
* to reload the correct form
*/
getSummonDataSpeciesFormIndex(): number {
if (this.summonData.speciesForm) {
return this.summonData.speciesForm.formIndex;
}
return 0;
}
}

View File

@ -1,5 +1,7 @@
import { PokeballType } from "#app/enums/pokeball";
import { WeatherType } from "#app/enums/weather-type";
import type { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#app/ui/command-ui-handler";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -131,4 +133,20 @@ describe("Abilities - Desolate Land", () => {
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN);
});
it("should lift after fleeing from a wild pokemon", async () => {
game.override
.enemyAbility(Abilities.DESOLATE_LAND)
.ability(Abilities.BALL_FETCH);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(0);
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN);
});
});

View File

@ -127,4 +127,63 @@ describe("Abilities - Imposter", () => {
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
it("should persist transformed attributes across reloads", async () => {
game.override.moveset([Moves.ABSORB]);
await game.classicMode.startBattle([Species.DITTO]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.toNextWave();
expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase");
expect(game.scene.currentBattle.waveIndex).toBe(2);
await game.reload.reloadSession();
const playerReloaded = game.scene.getPlayerPokemon()!;
const playerMoveset = player.getMoveset();
expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
expect(playerReloaded.getAbility()).toBe(enemy.getAbility());
expect(playerReloaded.getGender()).toBe(enemy.getGender());
expect(playerReloaded.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP));
for (const s of EFFECTIVE_STATS) {
expect(playerReloaded.getStat(s, false)).toBe(enemy.getStat(s, false));
}
expect(playerMoveset.length).toEqual(1);
expect(playerMoveset[0]?.moveId).toEqual(Moves.SPLASH);
});
it("should stay transformed with the correct form after reload", async () => {
game.override.moveset([Moves.ABSORB]);
game.override.enemySpecies(Species.UNOWN);
await game.classicMode.startBattle([Species.DITTO]);
const enemy = game.scene.getEnemyPokemon()!;
// change form
enemy.species.forms[5];
enemy.species.formIndex = 5;
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.toNextWave();
expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase");
expect(game.scene.currentBattle.waveIndex).toBe(2);
await game.reload.reloadSession();
const playerReloaded = game.scene.getPlayerPokemon()!;
expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex);
});
});

View File

@ -1,4 +1,6 @@
import { BattlerIndex } from "#app/battle";
import type { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#app/ui/command-ui-handler";
import { PostSummonWeatherChangeAbAttr } from "#app/data/ability";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
@ -157,6 +159,22 @@ describe("Abilities - Neutralizing Gas", () => {
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined();
});
it("should deactivate after fleeing from a wild pokemon", async () => {
game.override
.enemyAbility(Abilities.NEUTRALIZING_GAS)
.ability(Abilities.BALL_FETCH);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined();
vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(0);
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined();
});
it("should not activate abilities of pokemon no longer on the field", async () => {
game.override
.battleType("single")

View File

@ -0,0 +1,159 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/moves/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import type { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Items - Reviver Seed", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH, Moves.TACKLE, Moves.ENDURE ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.startingHeldItems([{ name: "REVIVER_SEED" }])
.enemyHeldItems([{ name: "REVIVER_SEED" }])
.enemyMoveset(Moves.SPLASH);
vi.spyOn(allMoves[Moves.SHEER_COLD], "accuracy", "get").mockReturnValue(100);
vi.spyOn(allMoves[Moves.LEECH_SEED], "accuracy", "get").mockReturnValue(100);
vi.spyOn(allMoves[Moves.WHIRLPOOL], "accuracy", "get").mockReturnValue(100);
vi.spyOn(allMoves[Moves.WILL_O_WISP], "accuracy", "get").mockReturnValue(100);
});
it.each([
{ moveType: "Special Move", move: Moves.WATER_GUN },
{ moveType: "Physical Move", move: Moves.TACKLE },
{ moveType: "Fixed Damage Move", move: Moves.SEISMIC_TOSS },
{ moveType: "Final Gambit", move: Moves.FINAL_GAMBIT },
{ moveType: "Counter", move: Moves.COUNTER },
{ moveType: "OHKO", move: Moves.SHEER_COLD }
])("should activate the holder's reviver seed from a $moveType", async ({ move }) => {
game.override
.enemyLevel(100)
.startingLevel(1)
.enemyMoveset(move);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(reviverSeed, "apply");
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("BerryPhase");
expect(player.isFainted()).toBeFalsy();
});
it("should activate the holder's reviver seed from confusion self-hit", async () => {
game.override
.enemyLevel(1)
.startingLevel(100)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
player.addTag(BattlerTagType.CONFUSED, 3);
const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(reviverSeed, "apply");
vi.spyOn(player, "randSeedInt").mockReturnValue(0); // Force confusion self-hit
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("BerryPhase");
expect(player.isFainted()).toBeFalsy();
});
// Damaging opponents tests
it.each([
{ moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE },
{ moveType: "Chip Damage", move: Moves.LEECH_SEED },
{ moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL },
{ moveType: "Status Effect Damage", move: Moves.WILL_O_WISP },
{ moveType: "Weather", move: Moves.SANDSTORM },
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
game.override
.enemyLevel(1)
.startingLevel(100)
.enemySpecies(Species.MAGIKARP)
.moveset(move)
.enemyMoveset(Moves.ENDURE);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
enemy.damageAndUpdate(enemy.hp - 1);
game.move.select(move);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.isFainted()).toBeTruthy();
});
// Self-damage tests
it.each([
{ moveType: "Recoil", move: Moves.DOUBLE_EDGE },
{ moveType: "Self-KO", move: Moves.EXPLOSION },
{ moveType: "Self-Deduction", move: Moves.CURSE },
{ moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN },
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
game.override
.enemyLevel(100)
.startingLevel(1)
.enemySpecies(Species.MAGIKARP)
.moveset(move)
.enemyAbility(Abilities.LIQUID_OOZE)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.GASTLY, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
const playerSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(playerSeed, "apply");
game.move.select(move);
await game.phaseInterceptor.to("TurnEndPhase");
expect(player.isFainted()).toBeTruthy();
});
it("should not activate the holder's reviver seed from Destiny Bond fainting", async () => {
game.override
.enemyLevel(100)
.startingLevel(1)
.enemySpecies(Species.MAGIKARP)
.moveset(Moves.DESTINY_BOND)
.startingHeldItems([]) // reset held items to nothing so user doesn't revive and not trigger Destiny Bond
.enemyMoveset(Moves.TACKLE);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DESTINY_BOND);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.isFainted()).toBeTruthy();
});
});

View File

@ -22,7 +22,7 @@ describe("Moves - Endure", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.THUNDER, Moves.BULLET_SEED, Moves.TOXIC])
.moveset([ Moves.THUNDER, Moves.BULLET_SEED, Moves.TOXIC, Moves.SHEER_COLD ])
.ability(Abilities.SKILL_LINK)
.startingLevel(100)
.battleType("single")
@ -50,16 +50,37 @@ describe("Moves - Endure", () => {
expect(game.scene.getEnemyPokemon()!.hp).toBe(1);
});
it("shouldn't prevent fainting from indirect damage", async () => {
game.override.enemyLevel(100);
await game.classicMode.startBattle([Species.ARCEUS]);
it("should let the pokemon survive against OHKO moves", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 2;
game.move.select(Moves.TOXIC);
await game.phaseInterceptor.to("VictoryPhase");
game.move.select(Moves.SHEER_COLD);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.isFainted()).toBe(true);
expect(enemy.isFainted()).toBeFalsy();
});
// comprehensive indirect damage test copied from Reviver Seed test
it.each([
{ moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE },
{ moveType: "Chip Damage", move: Moves.LEECH_SEED },
{ moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL },
{ moveType: "Status Effect Damage", move: Moves.TOXIC },
{ moveType: "Weather", move: Moves.SANDSTORM },
])("should not prevent fainting from $moveType", async ({ move }) => {
game.override
.enemyLevel(1)
.startingLevel(100)
.enemySpecies(Species.MAGIKARP)
.moveset(move)
.enemyMoveset(Moves.ENDURE);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
enemy.damageAndUpdate(enemy.hp - 1);
game.move.select(move);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.isFainted()).toBeTruthy();
});
});

View File

@ -114,4 +114,25 @@ describe("Moves - Revival Blessing", () => {
expect(feebas.hp).toBe(toDmgValue(0.5 * feebas.getMaxHp()));
expect(game.scene.getPlayerField()[0]).toBe(feebas);
});
it("should not summon multiple pokemon to the same slot when reviving the enemy ally in doubles", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.REVIVAL_BLESSING ])
.moveset([ Moves.SPLASH ])
.startingWave(25); // 2nd rival battle - must have 3+ pokemon
await game.classicMode.startBattle([ Species.ARCEUS, Species.GIRATINA ]);
const enemyFainting = game.scene.getEnemyField()[0];
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.SPLASH, 1);
await game.killPokemon(enemyFainting);
await game.phaseInterceptor.to("BerryPhase");
await game.toNextTurn();
// If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3
// Make sure it's still in slot 1
expect(game.scene.getEnemyParty()[0]).toBe(enemyFainting);
});
});

View File

@ -6,6 +6,7 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { BattlerIndex } from "#app/battle";
// TODO: Add more tests once Transform is fully implemented
describe("Moves - Transform", () => {
@ -58,7 +59,7 @@ describe("Moves - Transform", () => {
}
const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset();
const enemyMoveset = enemy.getMoveset();
expect(playerMoveset.length).toBe(enemyMoveset.length);
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
@ -127,4 +128,71 @@ describe("Moves - Transform", () => {
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
});
it("should persist transformed attributes across reloads", async () => {
game.override.enemyMoveset([]).moveset([]);
await game.classicMode.startBattle([Species.DITTO]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.move.changeMoveset(player, Moves.TRANSFORM);
game.move.changeMoveset(enemy, Moves.MEMENTO);
game.move.select(Moves.TRANSFORM);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextWave();
expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase");
expect(game.scene.currentBattle.waveIndex).toBe(2);
await game.reload.reloadSession();
const playerReloaded = game.scene.getPlayerPokemon()!;
const playerMoveset = player.getMoveset();
expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
expect(playerReloaded.getAbility()).toBe(enemy.getAbility());
expect(playerReloaded.getGender()).toBe(enemy.getGender());
expect(playerReloaded.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP));
for (const s of EFFECTIVE_STATS) {
expect(playerReloaded.getStat(s, false)).toBe(enemy.getStat(s, false));
}
expect(playerMoveset.length).toEqual(1);
expect(playerMoveset[0]?.moveId).toEqual(Moves.MEMENTO);
});
it("should stay transformed with the correct form after reload", async () => {
game.override.enemyMoveset([]).moveset([]);
game.override.enemySpecies(Species.DARMANITAN);
await game.classicMode.startBattle([Species.DITTO]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
// change form
enemy.species.forms[1];
enemy.species.formIndex = 1;
game.move.changeMoveset(player, Moves.TRANSFORM);
game.move.changeMoveset(enemy, Moves.MEMENTO);
game.move.select(Moves.TRANSFORM);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextWave();
expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase");
expect(game.scene.currentBattle.waveIndex).toBe(2);
await game.reload.reloadSession();
const playerReloaded = game.scene.getPlayerPokemon()!;
expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId);
expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex);
});
});