mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 15:03:24 +02:00
Merge 69e78f1b40
into 8d5ba221d8
This commit is contained in:
commit
1ac4433deb
@ -11,8 +11,8 @@ import type { AbAttr } from "#abilities/ability";
|
||||
*
|
||||
* ⚠️ Should never be used with `extends`, as this will nullify the exactness of the type.
|
||||
*
|
||||
* As an example, used to ensure that the parameters of {@linkcode AbAttr.canApply} and {@linkcode AbAttr.getTriggerMessage} are compatible with
|
||||
* the type of its {@linkcode AbAttr.apply | apply} method.
|
||||
* As an example, used to ensure that the parameters of {@linkcode AbAttr.canApply} and {@linkcode AbAttr.getTriggerMessage}
|
||||
* are compatible with the type of its {@linkcode AbAttr.apply | apply} method.
|
||||
*
|
||||
* @typeParam T - The type to match exactly
|
||||
*/
|
||||
@ -27,16 +27,17 @@ export type Exact<T> = {
|
||||
export type Closed<X> = X;
|
||||
|
||||
/**
|
||||
* Remove `readonly` from all properties of the provided type.
|
||||
* @typeParam T - The type to make mutable.
|
||||
* Helper type to strip `readonly` from all properties of the provided type.
|
||||
* Inverse of {@linkcode Readonly}
|
||||
* @typeParam T - The type to make mutable
|
||||
*/
|
||||
export type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type helper to obtain the keys associated with a given value inside an object.
|
||||
* Acts similar to {@linkcode Pick}, except checking the object's values instead of its keys.
|
||||
* Type helper to obtain the keys associated with a given value inside an object. \
|
||||
* Functions similarly to `Pick`, but checking assignability of values instead of keys.
|
||||
* @typeParam O - The type of the object
|
||||
* @typeParam V - The type of one of O's values.
|
||||
*/
|
||||
@ -49,6 +50,7 @@ export type InferKeys<O extends object, V> = V extends ObjectValues<O>
|
||||
/**
|
||||
* Utility type to obtain a union of the values of a given object. \
|
||||
* Functions similar to `keyof E`, except producing the values instead of the keys.
|
||||
* @typeParam E - The type of the object
|
||||
* @remarks
|
||||
* This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
|
||||
*/
|
||||
@ -85,6 +87,7 @@ export type NonFunctionPropertiesRecursive<Class> = {
|
||||
: Class[K];
|
||||
};
|
||||
|
||||
/** Utility type for an abstract constructor. */
|
||||
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
@ -103,11 +106,12 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
|
||||
* of its properties be present.
|
||||
*
|
||||
* Distinct from {@linkcode Partial} as this requires at least 1 property to _not_ be undefined.
|
||||
* @typeParam T - The type to render partial
|
||||
* @typeParam T - The object type to render partial
|
||||
*/
|
||||
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
|
||||
|
||||
/** Type helper that adds a brand to a type, used for nominal typing.
|
||||
/**
|
||||
* Type helper that adds a brand to a type, used for nominal typing.
|
||||
*
|
||||
* @remarks
|
||||
* Brands should be either a string or unique symbol. This prevents overlap with other types.
|
||||
|
@ -1120,10 +1120,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr {
|
||||
|
||||
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
||||
const tag = globalScene.arena.getTag(this.arenaTagType) as EntryHazardTag;
|
||||
return (
|
||||
this.condition(pokemon, attacker, move)
|
||||
&& (!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers)
|
||||
);
|
||||
return this.condition(pokemon, attacker, move) && (!tag || tag.canAdd());
|
||||
}
|
||||
|
||||
override apply({ simulated, pokemon }: PostMoveInteractionAbAttrParams): void {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -871,7 +871,7 @@ export abstract class Move implements Localizable {
|
||||
|
||||
|
||||
if (!this.hasAttr("TypelessAttr")) {
|
||||
globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power);
|
||||
globalScene.arena.applyTags(WeakenMoveTypeTag, typeChangeHolder.value, power);
|
||||
globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, typeChangeHolder.value, power);
|
||||
}
|
||||
|
||||
@ -1343,7 +1343,7 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
|
||||
if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) {
|
||||
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
globalScene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance);
|
||||
globalScene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, moveChance);
|
||||
}
|
||||
|
||||
if (!selfEffect) {
|
||||
@ -6063,7 +6063,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr {
|
||||
if (!tag) {
|
||||
return true;
|
||||
}
|
||||
return tag.layers < tag.maxLayers;
|
||||
return tag.canAdd();
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6087,7 +6087,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
||||
if (!tag) {
|
||||
return true;
|
||||
}
|
||||
return tag.layers < tag.maxLayers;
|
||||
return tag.canAdd();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||
import type { NonSerializableArenaTagType, SerializableArenaTagType } from "#types/arena-tags";
|
||||
|
||||
/**
|
||||
* Enum representing all different types of {@linkcode ArenaTag}s.
|
||||
* @privateRemarks
|
||||
|
@ -7,7 +7,7 @@ import { globalScene } from "#app/global-scene";
|
||||
import Overrides from "#app/overrides";
|
||||
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
||||
import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } from "#balance/biomes";
|
||||
import type { ArenaTag } from "#data/arena-tag";
|
||||
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||
import { EntryHazardTag, getArenaTag } from "#data/arena-tag";
|
||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||
@ -651,16 +651,33 @@ export class Arena {
|
||||
|
||||
/**
|
||||
* Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter
|
||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
||||
* @param side {@linkcode ArenaTagSide} which side's arena tags to apply
|
||||
* @param simulated if `true`, this applies arena tags without changing game state
|
||||
* @param args array of parameters that the called upon tags may need
|
||||
* @param tagType - A constructor of an ArenaTag to filter tags by
|
||||
* @param side - The {@linkcode ArenaTagSide} dictating which side's arena tags to apply
|
||||
* @param args - Parameters for the tag
|
||||
* @privateRemarks
|
||||
* If you get errors mentioning incompatibility with overload signatures, review the arguments being passed
|
||||
* to ensure they are correct for the tag being used.
|
||||
*/
|
||||
applyTagsForSide(
|
||||
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
|
||||
applyTagsForSide<T extends ArenaTag>(
|
||||
tagType: Constructor<T> | AbstractConstructor<T>,
|
||||
side: ArenaTagSide,
|
||||
simulated: boolean,
|
||||
...args: unknown[]
|
||||
...args: Parameters<T["apply"]>
|
||||
): void;
|
||||
/**
|
||||
* Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter
|
||||
* @param tagType - The {@linkcode ArenaTagType} of the desired tag
|
||||
* @param side - The {@linkcode ArenaTagSide} dictating which side's arena tags to apply
|
||||
* @param args - Parameters for the tag
|
||||
*/
|
||||
applyTagsForSide<T extends ArenaTagType>(
|
||||
tagType: T,
|
||||
side: ArenaTagSide,
|
||||
...args: Parameters<ArenaTagTypeMap[T]["apply"]>
|
||||
): void;
|
||||
applyTagsForSide<T extends ArenaTag>(
|
||||
tagType: T["tagType"] | Constructor<T> | AbstractConstructor<ArenaTag>,
|
||||
side: ArenaTagSide,
|
||||
...args: Parameters<T["apply"]>
|
||||
): void {
|
||||
let tags =
|
||||
typeof tagType === "string"
|
||||
@ -669,22 +686,32 @@ export class Arena {
|
||||
if (side !== ArenaTagSide.BOTH) {
|
||||
tags = tags.filter(t => t.side === side);
|
||||
}
|
||||
tags.forEach(t => t.apply(this, simulated, ...args));
|
||||
tags.forEach(t => t.apply(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
|
||||
* by calling {@linkcode applyTagsForSide()}
|
||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
||||
* @param simulated if `true`, this applies arena tags without changing game state
|
||||
* @param args array of parameters that the called upon tags may need
|
||||
* @param tagType - The {@linkcode ArenaTagType} of the desired tag
|
||||
* @param args - Parameters for the tag
|
||||
*/
|
||||
applyTags(
|
||||
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
|
||||
simulated: boolean,
|
||||
...args: unknown[]
|
||||
): void {
|
||||
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args);
|
||||
applyTags<T extends ArenaTagType>(tagType: T, ...args: Parameters<ArenaTagTypeMap[T]["apply"]>): void;
|
||||
/**
|
||||
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
|
||||
* by calling {@linkcode applyTagsForSide()}
|
||||
* @param tagType - A constructor of an ArenaTag to filter tags by
|
||||
* @param args - Parameters for the tag
|
||||
*/
|
||||
applyTags<T extends ArenaTag>(
|
||||
tagType: Constructor<T> | AbstractConstructor<T>,
|
||||
...args: Parameters<T["apply"]>
|
||||
): void;
|
||||
applyTags<T extends ArenaTag>(
|
||||
tagType: T["tagType"] | Constructor<T> | AbstractConstructor<ArenaTag>,
|
||||
...args: Parameters<T["apply"]>
|
||||
) {
|
||||
// @ts-expect-error - Overload resolution
|
||||
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -708,7 +735,7 @@ export class Arena {
|
||||
): boolean {
|
||||
const existingTag = this.getTagOnSide(tagType, side);
|
||||
if (existingTag) {
|
||||
existingTag.onOverlap(this, globalScene.getPokemonById(sourceId));
|
||||
existingTag.onOverlap(globalScene.getPokemonById(sourceId));
|
||||
|
||||
if (existingTag instanceof EntryHazardTag) {
|
||||
const { tagType, side, turnCount, maxDuration, layers, maxLayers } = existingTag as EntryHazardTag;
|
||||
@ -721,7 +748,7 @@ export class Arena {
|
||||
// creates a new tag object
|
||||
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, side);
|
||||
if (newTag) {
|
||||
newTag.onAdd(this, quiet);
|
||||
newTag.onAdd(quiet);
|
||||
this.tags.push(newTag);
|
||||
|
||||
const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {};
|
||||
@ -801,9 +828,9 @@ export class Arena {
|
||||
|
||||
lapseTags(): void {
|
||||
this.tags
|
||||
.filter(t => !t.lapse(this))
|
||||
.filter(t => !t.lapse())
|
||||
.forEach(t => {
|
||||
t.onRemove(this);
|
||||
t.onRemove();
|
||||
this.tags.splice(this.tags.indexOf(t), 1);
|
||||
|
||||
this.eventTarget.dispatchEvent(new TagRemovedEvent(t.tagType, t.side, t.turnCount));
|
||||
@ -814,7 +841,7 @@ export class Arena {
|
||||
const tags = this.tags;
|
||||
const tag = tags.find(t => t.tagType === tagType);
|
||||
if (tag) {
|
||||
tag.onRemove(this);
|
||||
tag.onRemove();
|
||||
tags.splice(tags.indexOf(tag), 1);
|
||||
|
||||
this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount));
|
||||
@ -825,7 +852,7 @@ export class Arena {
|
||||
removeTagOnSide(tagType: ArenaTagType, side: ArenaTagSide, quiet = false): boolean {
|
||||
const tag = this.getTagOnSide(tagType, side);
|
||||
if (tag) {
|
||||
tag.onRemove(this, quiet);
|
||||
tag.onRemove(quiet);
|
||||
this.tags.splice(this.tags.indexOf(tag), 1);
|
||||
|
||||
this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount));
|
||||
@ -835,7 +862,7 @@ export class Arena {
|
||||
|
||||
removeAllTags(): void {
|
||||
while (this.tags.length > 0) {
|
||||
this.tags[0].onRemove(this);
|
||||
this.tags[0].onRemove();
|
||||
this.eventTarget.dispatchEvent(
|
||||
new TagRemovedEvent(this.tags[0].tagType, this.tags[0].side, this.tags[0].turnCount),
|
||||
);
|
||||
|
@ -2397,7 +2397,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return moveTypeHolder.value as PokemonType;
|
||||
}
|
||||
|
||||
globalScene.arena.applyTags(ArenaTagType.ION_DELUGE, simulated, moveTypeHolder);
|
||||
globalScene.arena.applyTags(ArenaTagType.ION_DELUGE, moveTypeHolder);
|
||||
if (this.getTag(BattlerTagType.ELECTRIFIED)) {
|
||||
moveTypeHolder.value = PokemonType.ELECTRIC;
|
||||
}
|
||||
@ -3704,14 +3704,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// Critical hits should bypass screens
|
||||
if (!isCritical) {
|
||||
globalScene.arena.applyTagsForSide(
|
||||
WeakenMoveScreenTag,
|
||||
defendingSide,
|
||||
simulated,
|
||||
source,
|
||||
moveCategory,
|
||||
screenMultiplier,
|
||||
);
|
||||
globalScene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, source, moveCategory, screenMultiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3849,11 +3842,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
// apply crit block effects from lucky chant & co., overriding previous effects
|
||||
const blockCrit = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockCritAbAttr", { pokemon: this, blockCrit });
|
||||
const blockCritTag = globalScene.arena.getTagOnSide(
|
||||
globalScene.arena.applyTagsForSide(
|
||||
NoCritTag,
|
||||
this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY,
|
||||
blockCrit,
|
||||
);
|
||||
isCritical &&= !blockCritTag && !blockCrit.value; // need to roll a crit and not be blocked by either crit prevention effect
|
||||
isCritical &&= !blockCrit.value; // need to roll a crit and not be blocked by either crit prevention effect
|
||||
|
||||
return isCritical;
|
||||
}
|
||||
|
@ -5,9 +5,10 @@ import i18next from "i18next";
|
||||
|
||||
/**
|
||||
* Retrieves the Pokemon's name, potentially with an affix indicating its role (wild or foe) in the current battle context, translated
|
||||
* @param pokemon {@linkcode Pokemon} name and battle context will be retrieved from this instance
|
||||
* @param useIllusion - Whether we want the name of the illusion or not. Default value : true
|
||||
* @returns ex: "Wild Gengar", "Ectoplasma sauvage"
|
||||
* @param pokemon - The {@linkcode Pokemon} to retrieve the name of. Will return 'Missingno' as a fallback if null/undefined
|
||||
* @param useIllusion - Whether we want the name of the illusion or not; default `true`
|
||||
* @returns The localized name of `pokemon` complete with affix. Ex: "Wild Gengar", "Ectoplasma sauvage"
|
||||
* @todo Remove this and switch to using i18n context selectors based on pokemon trainer class - this causes incorrect locales
|
||||
*/
|
||||
export function getPokemonNameWithAffix(pokemon: Pokemon | undefined, useIllusion = true): string {
|
||||
if (!pokemon) {
|
||||
|
@ -18,7 +18,7 @@ export class TurnStartPhase extends FieldPhase {
|
||||
* Returns an ordering of the current field based on command priority
|
||||
* @returns The sequence of commands for this turn
|
||||
*/
|
||||
getCommandOrder(): BattlerIndex[] {
|
||||
private getCommandOrder(): BattlerIndex[] {
|
||||
const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex());
|
||||
const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex());
|
||||
const orderedTargets: BattlerIndex[] = playerField.concat(enemyField);
|
||||
|
@ -49,8 +49,7 @@ function sortBySpeed<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
|
||||
|
||||
/** 'true' if Trick Room is on the field. */
|
||||
const speedReversed = new BooleanHolder(false);
|
||||
globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed);
|
||||
|
||||
globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, speedReversed);
|
||||
if (speedReversed.value) {
|
||||
pokemonList.reverse();
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
expect(
|
||||
game.scene.arena
|
||||
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
||||
?.getSourcePokemon()
|
||||
?.["getSourcePokemon"]()
|
||||
?.getBattlerIndex(),
|
||||
).toBe(BattlerIndex.ENEMY);
|
||||
game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true);
|
||||
@ -319,7 +319,7 @@ describe("Abilities - Magic Bounce", () => {
|
||||
expect(
|
||||
game.scene.arena
|
||||
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
||||
?.getSourcePokemon()
|
||||
?.["getSourcePokemon"]()
|
||||
?.getBattlerIndex(),
|
||||
).toBe(BattlerIndex.ENEMY);
|
||||
});
|
||||
|
106
test/arena/arena-tags.test.ts
Normal file
106
test/arena/arena-tags.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Arena Tags", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let playerId: number;
|
||||
|
||||
afterAll(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.BLISSEY)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.battleType(BattleType.TRAINER);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MORELULL]);
|
||||
|
||||
playerId = game.field.getPlayerPokemon().id;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the message queue function to not unshift phases and just spit the text out directly
|
||||
vi.spyOn(game.scene.phaseManager, "queueMessage").mockImplementation((text, callbackDelay, prompt, promptDelay) =>
|
||||
game.scene.ui.showText(text, null, null, callbackDelay, prompt, promptDelay),
|
||||
);
|
||||
game.textInterceptor.clearLogs();
|
||||
});
|
||||
|
||||
// These tags are either ineligible or just jaaaaaaaaaaank
|
||||
const FORBIDDEN_TAGS = [ArenaTagType.NONE, ArenaTagType.NEUTRALIZING_GAS] as const;
|
||||
|
||||
const sides = getEnumValues(ArenaTagSide);
|
||||
const arenaTags = Object.values(ArenaTagType)
|
||||
.filter(t => !(FORBIDDEN_TAGS as readonly ArenaTagType[]).includes(t))
|
||||
.flatMap(t =>
|
||||
sides.map(side => ({
|
||||
tagType: t,
|
||||
name: toTitleCase(t),
|
||||
side,
|
||||
sideName: getEnumStr(ArenaTagSide, side),
|
||||
})),
|
||||
);
|
||||
|
||||
it.each(arenaTags)(
|
||||
"$name should display a message on addition, and a separate one on removal - $sideName",
|
||||
({ tagType, side }) => {
|
||||
game.scene.arena.addTag(tagType, 0, undefined, playerId, side);
|
||||
|
||||
expect(game).toHaveArenaTag(tagType, side);
|
||||
const tag = game.scene.arena.getTagOnSide(tagType, side)!;
|
||||
|
||||
if (tag["onAddMessageKey"]) {
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t(tag["onAddMessageKey"], {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(tag["getSourcePokemon"]()),
|
||||
moveName: tag["getMoveName"](),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
expect(game.textInterceptor.logs).toHaveLength(0);
|
||||
}
|
||||
|
||||
game.textInterceptor.clearLogs();
|
||||
|
||||
game.scene.arena.removeTagOnSide(tagType, side, false);
|
||||
if (tag["onRemoveMessageKey"]) {
|
||||
// TODO: Convert to `game.toHaveShownMessage`
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t(tag["onRemoveMessageKey"], {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(tag["getSourcePokemon"]()),
|
||||
moveName: tag["getMoveName"](),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
expect(game.textInterceptor.logs).toHaveLength(0);
|
||||
}
|
||||
|
||||
expect(game).not.toHaveArenaTag(tagType, side);
|
||||
},
|
||||
);
|
||||
});
|
@ -140,14 +140,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
|
||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
if (globalScene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
||||
globalScene.arena.applyTagsForSide(
|
||||
ArenaTagType.AURORA_VEIL,
|
||||
side,
|
||||
false,
|
||||
attacker,
|
||||
move.category,
|
||||
multiplierHolder,
|
||||
);
|
||||
globalScene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, attacker, move.category, multiplierHolder);
|
||||
}
|
||||
|
||||
return move.power * multiplierHolder.value;
|
||||
|
95
test/moves/ceaseless-edge-stone-axe.test.ts
Normal file
95
test/moves/ceaseless-edge-stone-axe.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import type { EntryHazardTagType } from "#types/arena-tags";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe.each<{ name: string; move: MoveId; hazard: EntryHazardTagType; hazardName: string }>([
|
||||
{ name: "Ceaseless Edge", move: MoveId.CEASELESS_EDGE, hazard: ArenaTagType.SPIKES, hazardName: "spikes" },
|
||||
{ name: "Stone Axe", move: MoveId.STONE_AXE, hazard: ArenaTagType.STEALTH_ROCK, hazardName: "stealth rock" },
|
||||
])("Move - $name", ({ move, hazard, hazardName }) => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.RATTATA)
|
||||
.ability(AbilityId.NO_GUARD)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it(`should hit and apply ${hazardName}`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
game.move.use(move);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
expect(game).not.toHaveArenaTag(hazard, ArenaTagSide.ENEMY);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game).toHaveArenaTag({ tagType: hazard, side: ArenaTagSide.ENEMY, layers: 1 });
|
||||
expect(game.field.getEnemyPokemon()).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
const maxLayers = hazard === ArenaTagType.SPIKES ? 3 : 1;
|
||||
|
||||
it(`should not fail if ${hazardName} already has ${maxLayers} layer${maxLayers === 1 ? "" : "s"}`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
for (let i = 0; i < maxLayers; i++) {
|
||||
game.scene.arena.addTag(hazard, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||
}
|
||||
expect(game).toHaveArenaTag({ tagType: hazard, side: ArenaTagSide.ENEMY, layers: maxLayers });
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Should not have increased due to already being at max layers
|
||||
expect(game).toHaveArenaTag({ tagType: hazard, side: ArenaTagSide.ENEMY, layers: maxLayers });
|
||||
const illumise = game.field.getPlayerPokemon();
|
||||
expect(illumise).toHaveUsedMove({ move, result: MoveResult.SUCCESS });
|
||||
expect(game.field.getEnemyPokemon()).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
it.runIf(move === MoveId.CEASELESS_EDGE)(
|
||||
"should apply 1 layer of spikes per hit when given multiple hits",
|
||||
async () => {
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS" }]);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
game.move.use(MoveId.CEASELESS_EDGE);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
// Hit 1
|
||||
expect(game).toHaveArenaTag({ tagType: ArenaTagType.SPIKES, side: ArenaTagSide.ENEMY, layers: 1 });
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Hit 2
|
||||
expect(game).toHaveArenaTag({ tagType: ArenaTagType.SPIKES, side: ArenaTagSide.ENEMY, layers: 2 });
|
||||
expect(game.field.getEnemyPokemon()).not.toHaveFullHp();
|
||||
},
|
||||
);
|
||||
});
|
@ -1,108 +0,0 @@
|
||||
import { EntryHazardTag } from "#data/arena-tag";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
describe("Moves - Ceaseless Edge", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(SpeciesId.RATTATA)
|
||||
.enemyAbility(AbilityId.RUN_AWAY)
|
||||
.enemyPassiveAbility(AbilityId.RUN_AWAY)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.moveset([MoveId.CEASELESS_EDGE, MoveId.SPLASH, MoveId.ROAR])
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
vi.spyOn(allMoves[MoveId.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
test("move should hit and apply spikes", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(MoveId.CEASELESS_EDGE);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||
expect(tagAfter.layers).toBe(1);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
});
|
||||
|
||||
test("move should hit twice with multi lens and apply two layers of spikes", async () => {
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS" }]);
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(MoveId.CEASELESS_EDGE);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||
expect(tagAfter.layers).toBe(2);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
});
|
||||
|
||||
test("trainer - move should hit twice, apply two layers of spikes, force switch opponent - opponent takes damage", async () => {
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS" }]).startingWave(25);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||
|
||||
game.move.select(MoveId.CEASELESS_EDGE);
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
// Spikes should not have any layers before move effect is applied
|
||||
const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagBefore instanceof EntryHazardTag).toBeFalsy();
|
||||
|
||||
await game.toNextTurn();
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter instanceof EntryHazardTag).toBeTruthy();
|
||||
expect(tagAfter.layers).toBe(2);
|
||||
|
||||
const hpBeforeSpikes = game.scene.currentBattle.enemyParty[1].hp;
|
||||
// Check HP of pokemon that WILL BE switched in (index 1)
|
||||
game.forceEnemyToSwitch();
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(hpBeforeSpikes);
|
||||
});
|
||||
});
|
@ -191,9 +191,7 @@ describe("Moves - Destiny Bond", () => {
|
||||
expect(playerPokemon.isFainted()).toBe(true);
|
||||
|
||||
// Ceaseless Edge spikes effect should still activate
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
||||
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
|
||||
expect(tagAfter.layers).toBe(1);
|
||||
expect(game).toHaveArenaTag({ tagType: ArenaTagType.SPIKES, side: ArenaTagSide.ENEMY, layers: 1 });
|
||||
});
|
||||
|
||||
it("should not cause a crash if the user is KO'd by Pledge moves", async () => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import type { TypeDamageMultiplier } from "#data/type";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
@ -147,7 +146,7 @@ describe("Moves - Entry Hazards", () => {
|
||||
it.each<{ name: string; layers: number; status: StatusEffect }>([
|
||||
{ name: "Poison", layers: 1, status: StatusEffect.POISON },
|
||||
{ name: "Toxic", layers: 2, status: StatusEffect.TOXIC },
|
||||
])("should apply $name at $layers without displaying neutralization msg", async ({ layers, status }) => {
|
||||
])("should apply $name at $layers", async ({ layers, status }) => {
|
||||
for (let i = 0; i < layers; i++) {
|
||||
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||
}
|
||||
@ -155,31 +154,20 @@ describe("Moves - Entry Hazards", () => {
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy).toHaveStatusEffect(status);
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
|
||||
moveName: allMoves[MoveId.TOXIC_SPIKES].name,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be removed without triggering upon a grounded Poison-type switching in", async () => {
|
||||
it("should be removed upon a grounded Poison-type switching in", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]);
|
||||
|
||||
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY);
|
||||
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.PLAYER);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
const ekans = game.field.getPlayerPokemon();
|
||||
expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER);
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(ekans),
|
||||
moveName: allMoves[MoveId.TOXIC_SPIKES].name,
|
||||
}),
|
||||
);
|
||||
expect(game.textInterceptor.logs).toContain(i18next.t("arenaTag:toxicSpikesOnRemovePlayer"));
|
||||
expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON);
|
||||
});
|
||||
|
||||
@ -225,7 +213,7 @@ describe("Moves - Entry Hazards", () => {
|
||||
expect(enemy).toHaveStatStage(Stat.SPD, -1);
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
||||
pokemonName: enemy.getNameToRender(),
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -127,15 +127,8 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
|
||||
const multiplierHolder = new NumberHolder(1);
|
||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
if (globalScene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
||||
globalScene.arena.applyTagsForSide(
|
||||
ArenaTagType.LIGHT_SCREEN,
|
||||
side,
|
||||
false,
|
||||
attacker,
|
||||
move.category,
|
||||
multiplierHolder,
|
||||
);
|
||||
if (globalScene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side) && !move.hasAttr("CritOnlyAttr")) {
|
||||
globalScene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, attacker, move.category, multiplierHolder);
|
||||
}
|
||||
|
||||
return move.power * multiplierHolder.value;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
@ -6,6 +7,7 @@ import { BerryPhase } from "#phases/berry-phase";
|
||||
import { CommandPhase } from "#phases/command-phase";
|
||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
@ -47,6 +49,11 @@ describe("Moves - Mat Block", () => {
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("arenaTags:matBlockApply", {
|
||||
attackName: allMoves[MoveId.TACKLE].name,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should not protect the user and allies from status moves", async () => {
|
||||
|
@ -144,7 +144,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
|
||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
if (globalScene.arena.getTagOnSide(ArenaTagType.REFLECT, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
||||
globalScene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder);
|
||||
globalScene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, attacker, move.category, multiplierHolder);
|
||||
}
|
||||
|
||||
return move.power * multiplierHolder.value;
|
||||
|
@ -5,6 +5,8 @@ import { enumValueToKey } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import type { MatcherState } from "@vitest/expect";
|
||||
import i18next from "i18next";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||
import type { describe, it } from "vitest";
|
||||
|
||||
type Casing = "Preserve" | "Title";
|
||||
|
||||
@ -22,15 +24,18 @@ interface getEnumStrOptions {
|
||||
* If present, will be added to the end of the enum string.
|
||||
*/
|
||||
suffix?: string;
|
||||
/**
|
||||
* Whether to omit the value from the text.
|
||||
* @defaultValue Whether `E` is a non-string enum
|
||||
*/
|
||||
omitValue?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of an enum member or const object value, alongside its corresponding value.
|
||||
* @param obj - The {@linkcode EnumOrObject} to source reverse mappings from
|
||||
* @param enums - One of {@linkcode obj}'s values
|
||||
* @param casing - A string denoting the casing method to use; default `Preserve`
|
||||
* @param prefix - An optional string to be prepended to the enum's string representation
|
||||
* @param suffix - An optional string to be appended to the enum's string representation
|
||||
* @param val - One of {@linkcode obj}'s values
|
||||
* @param options - Options modifying the stringification process.
|
||||
* @returns The stringified representation of `val` as dictated by the options.
|
||||
* @example
|
||||
* ```ts
|
||||
@ -46,8 +51,9 @@ interface getEnumStrOptions {
|
||||
export function getEnumStr<E extends EnumOrObject>(
|
||||
obj: E,
|
||||
val: ObjectValues<E>,
|
||||
{ casing = "Preserve", prefix = "", suffix = "" }: getEnumStrOptions = {},
|
||||
options: getEnumStrOptions = {},
|
||||
): string {
|
||||
const { casing = "Preserve", prefix = "", suffix = "", omitValue = typeof val === "number" } = options;
|
||||
let casingFunc: ((s: string) => string) | undefined;
|
||||
switch (casing) {
|
||||
case "Preserve":
|
||||
@ -68,7 +74,7 @@ export function getEnumStr<E extends EnumOrObject>(
|
||||
stringPart = casingFunc(stringPart);
|
||||
}
|
||||
|
||||
return `${prefix}${stringPart}${suffix} (=${val})`;
|
||||
return `${prefix}${stringPart}${suffix}${omitValue ? ` (=${val})` : ""}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,7 +93,7 @@ export function getEnumStr<E extends EnumOrObject>(
|
||||
* ```
|
||||
*/
|
||||
export function stringifyEnumArray<E extends EnumOrObject>(obj: E, enums: E[keyof E][]): string {
|
||||
if (obj.length === 0) {
|
||||
if (enums.length === 0) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user