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.
|
* ⚠️ 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
|
* As an example, used to ensure that the parameters of {@linkcode AbAttr.canApply} and {@linkcode AbAttr.getTriggerMessage}
|
||||||
* the type of its {@linkcode AbAttr.apply | apply} method.
|
* are compatible with the type of its {@linkcode AbAttr.apply | apply} method.
|
||||||
*
|
*
|
||||||
* @typeParam T - The type to match exactly
|
* @typeParam T - The type to match exactly
|
||||||
*/
|
*/
|
||||||
@ -27,16 +27,17 @@ export type Exact<T> = {
|
|||||||
export type Closed<X> = X;
|
export type Closed<X> = X;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove `readonly` from all properties of the provided type.
|
* Helper type to strip `readonly` from all properties of the provided type.
|
||||||
* @typeParam T - The type to make mutable.
|
* Inverse of {@linkcode Readonly}
|
||||||
|
* @typeParam T - The type to make mutable
|
||||||
*/
|
*/
|
||||||
export type Mutable<T> = {
|
export type Mutable<T> = {
|
||||||
-readonly [P in keyof T]: T[P];
|
-readonly [P in keyof T]: T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type helper to obtain the keys associated with a given value inside an object.
|
* 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.
|
* Functions similarly to `Pick`, but checking assignability of values instead of keys.
|
||||||
* @typeParam O - The type of the object
|
* @typeParam O - The type of the object
|
||||||
* @typeParam V - The type of one of O's values.
|
* @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. \
|
* 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.
|
* Functions similar to `keyof E`, except producing the values instead of the keys.
|
||||||
|
* @typeParam E - The type of the object
|
||||||
* @remarks
|
* @remarks
|
||||||
* This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
|
* 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];
|
: Class[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Utility type for an abstract constructor. */
|
||||||
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,11 +106,12 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
|
|||||||
* of its properties be present.
|
* of its properties be present.
|
||||||
*
|
*
|
||||||
* Distinct from {@linkcode Partial} as this requires at least 1 property to _not_ be undefined.
|
* 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> }>;
|
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
|
* @remarks
|
||||||
* Brands should be either a string or unique symbol. This prevents overlap with other types.
|
* 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 {
|
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean {
|
||||||
const tag = globalScene.arena.getTag(this.arenaTagType) as EntryHazardTag;
|
const tag = globalScene.arena.getTag(this.arenaTagType) as EntryHazardTag;
|
||||||
return (
|
return this.condition(pokemon, attacker, move) && (!tag || tag.canAdd());
|
||||||
this.condition(pokemon, attacker, move)
|
|
||||||
&& (!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override apply({ simulated, pokemon }: PostMoveInteractionAbAttrParams): void {
|
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")) {
|
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);
|
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")) {
|
if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) {
|
||||||
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
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) {
|
if (!selfEffect) {
|
||||||
@ -6063,7 +6063,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr {
|
|||||||
if (!tag) {
|
if (!tag) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return tag.layers < tag.maxLayers;
|
return tag.canAdd();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6087,7 +6087,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
|||||||
if (!tag) {
|
if (!tag) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return tag.layers < tag.maxLayers;
|
return tag.canAdd();
|
||||||
}
|
}
|
||||||
return false;
|
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.
|
* Enum representing all different types of {@linkcode ArenaTag}s.
|
||||||
* @privateRemarks
|
* @privateRemarks
|
||||||
|
@ -7,7 +7,7 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes";
|
||||||
import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } 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 { EntryHazardTag, getArenaTag } from "#data/arena-tag";
|
||||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
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
|
* 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 tagType - A constructor of an ArenaTag to filter tags by
|
||||||
* @param side {@linkcode ArenaTagSide} which side's arena tags to apply
|
* @param side - The {@linkcode ArenaTagSide} dictating which side's arena tags to apply
|
||||||
* @param simulated if `true`, this applies arena tags without changing game state
|
* @param args - Parameters for the tag
|
||||||
* @param args array of parameters that the called upon tags may need
|
* @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(
|
applyTagsForSide<T extends ArenaTag>(
|
||||||
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
|
tagType: Constructor<T> | AbstractConstructor<T>,
|
||||||
side: ArenaTagSide,
|
side: ArenaTagSide,
|
||||||
simulated: boolean,
|
...args: Parameters<T["apply"]>
|
||||||
...args: unknown[]
|
): 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 {
|
): void {
|
||||||
let tags =
|
let tags =
|
||||||
typeof tagType === "string"
|
typeof tagType === "string"
|
||||||
@ -669,22 +686,32 @@ export class Arena {
|
|||||||
if (side !== ArenaTagSide.BOTH) {
|
if (side !== ArenaTagSide.BOTH) {
|
||||||
tags = tags.filter(t => t.side === side);
|
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)
|
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
|
||||||
* by calling {@linkcode applyTagsForSide()}
|
* by calling {@linkcode applyTagsForSide()}
|
||||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
* @param tagType - The {@linkcode ArenaTagType} of the desired tag
|
||||||
* @param simulated if `true`, this applies arena tags without changing game state
|
* @param args - Parameters for the tag
|
||||||
* @param args array of parameters that the called upon tags may need
|
|
||||||
*/
|
*/
|
||||||
applyTags(
|
applyTags<T extends ArenaTagType>(tagType: T, ...args: Parameters<ArenaTagTypeMap[T]["apply"]>): void;
|
||||||
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
|
/**
|
||||||
simulated: boolean,
|
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
|
||||||
...args: unknown[]
|
* by calling {@linkcode applyTagsForSide()}
|
||||||
): void {
|
* @param tagType - A constructor of an ArenaTag to filter tags by
|
||||||
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args);
|
* @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 {
|
): boolean {
|
||||||
const existingTag = this.getTagOnSide(tagType, side);
|
const existingTag = this.getTagOnSide(tagType, side);
|
||||||
if (existingTag) {
|
if (existingTag) {
|
||||||
existingTag.onOverlap(this, globalScene.getPokemonById(sourceId));
|
existingTag.onOverlap(globalScene.getPokemonById(sourceId));
|
||||||
|
|
||||||
if (existingTag instanceof EntryHazardTag) {
|
if (existingTag instanceof EntryHazardTag) {
|
||||||
const { tagType, side, turnCount, maxDuration, layers, maxLayers } = existingTag as EntryHazardTag;
|
const { tagType, side, turnCount, maxDuration, layers, maxLayers } = existingTag as EntryHazardTag;
|
||||||
@ -721,7 +748,7 @@ export class Arena {
|
|||||||
// creates a new tag object
|
// creates a new tag object
|
||||||
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, side);
|
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, side);
|
||||||
if (newTag) {
|
if (newTag) {
|
||||||
newTag.onAdd(this, quiet);
|
newTag.onAdd(quiet);
|
||||||
this.tags.push(newTag);
|
this.tags.push(newTag);
|
||||||
|
|
||||||
const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {};
|
const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {};
|
||||||
@ -801,9 +828,9 @@ export class Arena {
|
|||||||
|
|
||||||
lapseTags(): void {
|
lapseTags(): void {
|
||||||
this.tags
|
this.tags
|
||||||
.filter(t => !t.lapse(this))
|
.filter(t => !t.lapse())
|
||||||
.forEach(t => {
|
.forEach(t => {
|
||||||
t.onRemove(this);
|
t.onRemove();
|
||||||
this.tags.splice(this.tags.indexOf(t), 1);
|
this.tags.splice(this.tags.indexOf(t), 1);
|
||||||
|
|
||||||
this.eventTarget.dispatchEvent(new TagRemovedEvent(t.tagType, t.side, t.turnCount));
|
this.eventTarget.dispatchEvent(new TagRemovedEvent(t.tagType, t.side, t.turnCount));
|
||||||
@ -814,7 +841,7 @@ export class Arena {
|
|||||||
const tags = this.tags;
|
const tags = this.tags;
|
||||||
const tag = tags.find(t => t.tagType === tagType);
|
const tag = tags.find(t => t.tagType === tagType);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
tag.onRemove(this);
|
tag.onRemove();
|
||||||
tags.splice(tags.indexOf(tag), 1);
|
tags.splice(tags.indexOf(tag), 1);
|
||||||
|
|
||||||
this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount));
|
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 {
|
removeTagOnSide(tagType: ArenaTagType, side: ArenaTagSide, quiet = false): boolean {
|
||||||
const tag = this.getTagOnSide(tagType, side);
|
const tag = this.getTagOnSide(tagType, side);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
tag.onRemove(this, quiet);
|
tag.onRemove(quiet);
|
||||||
this.tags.splice(this.tags.indexOf(tag), 1);
|
this.tags.splice(this.tags.indexOf(tag), 1);
|
||||||
|
|
||||||
this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount));
|
this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount));
|
||||||
@ -835,7 +862,7 @@ export class Arena {
|
|||||||
|
|
||||||
removeAllTags(): void {
|
removeAllTags(): void {
|
||||||
while (this.tags.length > 0) {
|
while (this.tags.length > 0) {
|
||||||
this.tags[0].onRemove(this);
|
this.tags[0].onRemove();
|
||||||
this.eventTarget.dispatchEvent(
|
this.eventTarget.dispatchEvent(
|
||||||
new TagRemovedEvent(this.tags[0].tagType, this.tags[0].side, this.tags[0].turnCount),
|
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;
|
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)) {
|
if (this.getTag(BattlerTagType.ELECTRIFIED)) {
|
||||||
moveTypeHolder.value = PokemonType.ELECTRIC;
|
moveTypeHolder.value = PokemonType.ELECTRIC;
|
||||||
}
|
}
|
||||||
@ -3704,14 +3704,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
// Critical hits should bypass screens
|
// Critical hits should bypass screens
|
||||||
if (!isCritical) {
|
if (!isCritical) {
|
||||||
globalScene.arena.applyTagsForSide(
|
globalScene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, source, moveCategory, screenMultiplier);
|
||||||
WeakenMoveScreenTag,
|
|
||||||
defendingSide,
|
|
||||||
simulated,
|
|
||||||
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
|
// apply crit block effects from lucky chant & co., overriding previous effects
|
||||||
const blockCrit = new BooleanHolder(false);
|
const blockCrit = new BooleanHolder(false);
|
||||||
applyAbAttrs("BlockCritAbAttr", { pokemon: this, blockCrit });
|
applyAbAttrs("BlockCritAbAttr", { pokemon: this, blockCrit });
|
||||||
const blockCritTag = globalScene.arena.getTagOnSide(
|
globalScene.arena.applyTagsForSide(
|
||||||
NoCritTag,
|
NoCritTag,
|
||||||
this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY,
|
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;
|
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
|
* 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 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 value : true
|
* @param useIllusion - Whether we want the name of the illusion or not; default `true`
|
||||||
* @returns ex: "Wild Gengar", "Ectoplasma sauvage"
|
* @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 {
|
export function getPokemonNameWithAffix(pokemon: Pokemon | undefined, useIllusion = true): string {
|
||||||
if (!pokemon) {
|
if (!pokemon) {
|
||||||
|
@ -18,7 +18,7 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
* Returns an ordering of the current field based on command priority
|
* Returns an ordering of the current field based on command priority
|
||||||
* @returns The sequence of commands for this turn
|
* @returns The sequence of commands for this turn
|
||||||
*/
|
*/
|
||||||
getCommandOrder(): BattlerIndex[] {
|
private getCommandOrder(): BattlerIndex[] {
|
||||||
const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex());
|
const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex());
|
||||||
const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex());
|
const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex());
|
||||||
const orderedTargets: BattlerIndex[] = playerField.concat(enemyField);
|
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. */
|
/** 'true' if Trick Room is on the field. */
|
||||||
const speedReversed = new BooleanHolder(false);
|
const speedReversed = new BooleanHolder(false);
|
||||||
globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed);
|
globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, speedReversed);
|
||||||
|
|
||||||
if (speedReversed.value) {
|
if (speedReversed.value) {
|
||||||
pokemonList.reverse();
|
pokemonList.reverse();
|
||||||
}
|
}
|
||||||
|
@ -307,7 +307,7 @@ describe("Abilities - Magic Bounce", () => {
|
|||||||
expect(
|
expect(
|
||||||
game.scene.arena
|
game.scene.arena
|
||||||
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
||||||
?.getSourcePokemon()
|
?.["getSourcePokemon"]()
|
||||||
?.getBattlerIndex(),
|
?.getBattlerIndex(),
|
||||||
).toBe(BattlerIndex.ENEMY);
|
).toBe(BattlerIndex.ENEMY);
|
||||||
game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true);
|
game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true);
|
||||||
@ -319,7 +319,7 @@ describe("Abilities - Magic Bounce", () => {
|
|||||||
expect(
|
expect(
|
||||||
game.scene.arena
|
game.scene.arena
|
||||||
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)
|
||||||
?.getSourcePokemon()
|
?.["getSourcePokemon"]()
|
||||||
?.getBattlerIndex(),
|
?.getBattlerIndex(),
|
||||||
).toBe(BattlerIndex.ENEMY);
|
).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;
|
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
|
||||||
if (globalScene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
if (globalScene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
||||||
globalScene.arena.applyTagsForSide(
|
globalScene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, attacker, move.category, multiplierHolder);
|
||||||
ArenaTagType.AURORA_VEIL,
|
|
||||||
side,
|
|
||||||
false,
|
|
||||||
attacker,
|
|
||||||
move.category,
|
|
||||||
multiplierHolder,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return move.power * multiplierHolder.value;
|
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);
|
expect(playerPokemon.isFainted()).toBe(true);
|
||||||
|
|
||||||
// Ceaseless Edge spikes effect should still activate
|
// Ceaseless Edge spikes effect should still activate
|
||||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag;
|
expect(game).toHaveArenaTag({ tagType: ArenaTagType.SPIKES, side: ArenaTagSide.ENEMY, layers: 1 });
|
||||||
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
|
|
||||||
expect(tagAfter.layers).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not cause a crash if the user is KO'd by Pledge moves", async () => {
|
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 { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { allMoves } from "#data/data-lists";
|
|
||||||
import type { TypeDamageMultiplier } from "#data/type";
|
import type { TypeDamageMultiplier } from "#data/type";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
@ -147,7 +146,7 @@ describe("Moves - Entry Hazards", () => {
|
|||||||
it.each<{ name: string; layers: number; status: StatusEffect }>([
|
it.each<{ name: string; layers: number; status: StatusEffect }>([
|
||||||
{ name: "Poison", layers: 1, status: StatusEffect.POISON },
|
{ name: "Poison", layers: 1, status: StatusEffect.POISON },
|
||||||
{ name: "Toxic", layers: 2, status: StatusEffect.TOXIC },
|
{ 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++) {
|
for (let i = 0; i < layers; i++) {
|
||||||
game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY);
|
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();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
expect(enemy).toHaveStatusEffect(status);
|
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]);
|
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);
|
game.doSwitchPokemon(1);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
const ekans = game.field.getPlayerPokemon();
|
const ekans = game.field.getPlayerPokemon();
|
||||||
expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER);
|
expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER);
|
||||||
expect(game.textInterceptor.logs).not.toContain(
|
expect(game.textInterceptor.logs).toContain(i18next.t("arenaTag:toxicSpikesOnRemovePlayer"));
|
||||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(ekans),
|
|
||||||
moveName: allMoves[MoveId.TOXIC_SPIKES].name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON);
|
expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -225,7 +213,7 @@ describe("Moves - Entry Hazards", () => {
|
|||||||
expect(enemy).toHaveStatStage(Stat.SPD, -1);
|
expect(enemy).toHaveStatStage(Stat.SPD, -1);
|
||||||
expect(game.textInterceptor.logs).toContain(
|
expect(game.textInterceptor.logs).toContain(
|
||||||
i18next.t("arenaTag:stickyWebActivateTrap", {
|
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 multiplierHolder = new NumberHolder(1);
|
||||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
|
||||||
if (globalScene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
if (globalScene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side) && !move.hasAttr("CritOnlyAttr")) {
|
||||||
globalScene.arena.applyTagsForSide(
|
globalScene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, attacker, move.category, multiplierHolder);
|
||||||
ArenaTagType.LIGHT_SCREEN,
|
|
||||||
side,
|
|
||||||
false,
|
|
||||||
attacker,
|
|
||||||
move.category,
|
|
||||||
multiplierHolder,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return move.power * multiplierHolder.value;
|
return move.power * multiplierHolder.value;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
@ -6,6 +7,7 @@ import { BerryPhase } from "#phases/berry-phase";
|
|||||||
import { CommandPhase } from "#phases/command-phase";
|
import { CommandPhase } from "#phases/command-phase";
|
||||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import i18next from "i18next";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
@ -47,6 +49,11 @@ describe("Moves - Mat Block", () => {
|
|||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
await game.phaseInterceptor.to(BerryPhase, false);
|
||||||
|
|
||||||
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
|
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 () => {
|
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;
|
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
|
||||||
if (globalScene.arena.getTagOnSide(ArenaTagType.REFLECT, side) && move.getAttrs("CritOnlyAttr").length === 0) {
|
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;
|
return move.power * multiplierHolder.value;
|
||||||
|
@ -5,6 +5,8 @@ import { enumValueToKey } from "#utils/enums";
|
|||||||
import { toTitleCase } from "#utils/strings";
|
import { toTitleCase } from "#utils/strings";
|
||||||
import type { MatcherState } from "@vitest/expect";
|
import type { MatcherState } from "@vitest/expect";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { describe, it } from "vitest";
|
||||||
|
|
||||||
type Casing = "Preserve" | "Title";
|
type Casing = "Preserve" | "Title";
|
||||||
|
|
||||||
@ -22,15 +24,18 @@ interface getEnumStrOptions {
|
|||||||
* If present, will be added to the end of the enum string.
|
* If present, will be added to the end of the enum string.
|
||||||
*/
|
*/
|
||||||
suffix?: 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.
|
* 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 obj - The {@linkcode EnumOrObject} to source reverse mappings from
|
||||||
* @param enums - One of {@linkcode obj}'s values
|
* @param val - One of {@linkcode obj}'s values
|
||||||
* @param casing - A string denoting the casing method to use; default `Preserve`
|
* @param options - Options modifying the stringification process.
|
||||||
* @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
|
|
||||||
* @returns The stringified representation of `val` as dictated by the options.
|
* @returns The stringified representation of `val` as dictated by the options.
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
@ -46,8 +51,9 @@ interface getEnumStrOptions {
|
|||||||
export function getEnumStr<E extends EnumOrObject>(
|
export function getEnumStr<E extends EnumOrObject>(
|
||||||
obj: E,
|
obj: E,
|
||||||
val: ObjectValues<E>,
|
val: ObjectValues<E>,
|
||||||
{ casing = "Preserve", prefix = "", suffix = "" }: getEnumStrOptions = {},
|
options: getEnumStrOptions = {},
|
||||||
): string {
|
): string {
|
||||||
|
const { casing = "Preserve", prefix = "", suffix = "", omitValue = typeof val === "number" } = options;
|
||||||
let casingFunc: ((s: string) => string) | undefined;
|
let casingFunc: ((s: string) => string) | undefined;
|
||||||
switch (casing) {
|
switch (casing) {
|
||||||
case "Preserve":
|
case "Preserve":
|
||||||
@ -68,7 +74,7 @@ export function getEnumStr<E extends EnumOrObject>(
|
|||||||
stringPart = casingFunc(stringPart);
|
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 {
|
export function stringifyEnumArray<E extends EnumOrObject>(obj: E, enums: E[keyof E][]): string {
|
||||||
if (obj.length === 0) {
|
if (enums.length === 0) {
|
||||||
return "[]";
|
return "[]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user