mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-17 21:09:25 +02:00
Compare commits
70 Commits
94e4b29fd4
...
c7bbfacbb0
Author | SHA1 | Date | |
---|---|---|---|
|
c7bbfacbb0 | ||
|
d776df4b21 | ||
|
23271901cf | ||
|
1517e0512e | ||
|
3ffa13e6b7 | ||
|
d85bcf2324 | ||
|
7264f56fa1 | ||
|
c34134159d | ||
|
99e1163752 | ||
|
a82768f040 | ||
|
8b1b1cd38a | ||
|
d6c77e0afa | ||
|
9ee0bfb84b | ||
|
c36394b781 | ||
|
a1c718322f | ||
|
2df1eaff10 | ||
|
b3eea02779 | ||
|
b55872ef32 | ||
|
ff6ec7f945 | ||
|
af77e2ef7f | ||
|
6edf75ca42 | ||
|
bda0f39df0 | ||
|
1bfdd8f24b | ||
|
19c0fba6f4 | ||
|
259a9b328d | ||
|
1f9eaf49c6 | ||
|
e1c999566e | ||
|
1cb1797f3b | ||
|
e385c73188 | ||
|
4fd44e8029 | ||
|
d614588783 | ||
|
adeedb84d5 | ||
|
eb1696e93e | ||
|
7d0ee7a9ed | ||
|
d82c105d55 | ||
|
e7b5f5fe99 | ||
|
fefa8e408f | ||
|
903d1a33dd | ||
|
7f2766f832 | ||
|
c6c3cd9f3c | ||
|
0c3ae62d1e | ||
|
779c95ba93 | ||
|
0402b07122 | ||
|
ba39af1bbe | ||
|
d20a47d082 | ||
|
9301e6db87 | ||
|
c24f630caf | ||
|
14f5849502 | ||
|
374474720b | ||
|
2eae68785d | ||
|
c919ea78cb | ||
|
e40b1bd452 | ||
|
a179fdeac6 | ||
|
455f3b6be1 | ||
|
059b9b2a95 | ||
|
b9a4e631db | ||
|
3e2d050d70 | ||
|
8a10cc2037 | ||
|
c76739f629 | ||
|
6f676e4438 | ||
|
3bd10f09e9 | ||
|
8a915d2dc8 | ||
|
d456b3849a | ||
|
82920fb059 | ||
|
4d93958627 | ||
|
47c45bc63e | ||
|
db927e8adb | ||
|
9bfb1bba88 | ||
|
96e4bb5e0e | ||
|
45bbaf2b25 |
@ -90,9 +90,13 @@ If this feature requires new text, the text should be integrated into the code w
|
||||
- For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text.
|
||||
[Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice.
|
||||
- You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response.
|
||||
3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
3. Your locales should use the following format:
|
||||
- File names should be in `kebab-case`. Example: `trainer-names.json`
|
||||
- Key names should be in `camelCase`. Example: `aceTrainer`
|
||||
- If you make use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context), you need to use `snake_case` for the context key. Example: `aceTrainer_male`
|
||||
4. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
|
||||
5. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
||||
6. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
||||
|
||||
[^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates).
|
||||
If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle.
|
||||
|
@ -1238,7 +1238,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
|
||||
// TODO: Probably want to check against simulated here
|
||||
const effect =
|
||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||
attacker.trySetStatus(effect, true, pokemon);
|
||||
attacker.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2231,7 +2231,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
||||
apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
||||
const effect =
|
||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||
opponent.trySetStatus(effect, true, pokemon);
|
||||
opponent.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2386,7 +2386,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
|
||||
*/
|
||||
override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void {
|
||||
if (!simulated && sourcePokemon) {
|
||||
sourcePokemon.trySetStatus(effect, true, pokemon);
|
||||
sourcePokemon.trySetStatus(effect, pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3662,7 +3662,8 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
protected immuneEffects: StatusEffect[];
|
||||
|
||||
/**
|
||||
* @param immuneEffects - The status effects to which the Pokémon is immune.
|
||||
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||
* If none are provided, will block **all** status effects regardless of type.
|
||||
*/
|
||||
constructor(...immuneEffects: StatusEffect[]) {
|
||||
super();
|
||||
@ -3671,7 +3672,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
||||
}
|
||||
|
||||
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
||||
return (effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) || this.immuneEffects.includes(effect);
|
||||
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3723,6 +3724,11 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
|
||||
*/
|
||||
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
protected immuneEffects: StatusEffect[];
|
||||
|
||||
/**
|
||||
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||
* If none are provided, will block **all** status effects regardless of type.
|
||||
*/
|
||||
constructor(...immuneEffects: StatusEffect[]) {
|
||||
super();
|
||||
|
||||
@ -3731,7 +3737,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||
|
||||
override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
||||
return (
|
||||
(!cancelled.value && effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) ||
|
||||
(!cancelled.value && this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) ||
|
||||
this.immuneEffects.includes(effect)
|
||||
);
|
||||
}
|
||||
@ -3757,6 +3763,10 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
||||
*/
|
||||
private condition: (target: Pokemon, source: Pokemon | null) => boolean;
|
||||
|
||||
/**
|
||||
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||
* If none are provided, will block **all** status effects regardless of type.
|
||||
*/
|
||||
constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) {
|
||||
super(...immuneEffects);
|
||||
|
||||
@ -7482,8 +7492,7 @@ export function initAbilities() {
|
||||
.unsuppressable()
|
||||
.bypassFaint(),
|
||||
new Ability(AbilityId.CORROSION, 7)
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ])
|
||||
.edgeCase(), // Should poison itself with toxic orb.
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]),
|
||||
new Ability(AbilityId.COMATOSE, 7)
|
||||
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
|
@ -890,32 +890,31 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
this.#neutralized = true;
|
||||
if (globalScene.arena.removeTag(this.tagType)) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else if (!pokemon.status) {
|
||||
const toxic = this.layers > 1;
|
||||
if (
|
||||
pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||
// Neutralize the tag and remove it from the field.
|
||||
// Message cannot be moved to `onRemove` as that requires a reference to the neutralizing pokemon
|
||||
this.#neutralized = true;
|
||||
globalScene.arena.removeTagOnSide(this.tagType, this.side);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt to poison the target, suppressing any immunity messages that arise.
|
||||
const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC;
|
||||
return pokemon.trySetStatus(effect, null, undefined, this.getMoveName(), false, true);
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
|
@ -563,7 +563,7 @@ export class BeakBlastChargingTag extends BattlerTag {
|
||||
target: pokemon,
|
||||
})
|
||||
) {
|
||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, pokemon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -1509,7 +1509,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (!super.lapse(pokemon, lapseType)) {
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
||||
pokemon.trySetStatus(StatusEffect.SLEEP);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1859,7 +1859,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag {
|
||||
* @param user - The pokemon that is being attacked and has the tag
|
||||
*/
|
||||
override onContact(attacker: Pokemon, user: Pokemon): void {
|
||||
attacker.trySetStatus(this.#statusEffect, true, user);
|
||||
attacker.trySetStatus(this.#statusEffect, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2803,7 +2803,7 @@ export class GulpMissileTag extends SerializableBattlerTag {
|
||||
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
||||
} else {
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, pokemon);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -88,7 +88,7 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||
import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
@ -1184,8 +1184,9 @@ export abstract class MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* @virtual
|
||||
* @returns the {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} for this {@linkcode Move}
|
||||
* Return this `MoveAttr`'s associated {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}.
|
||||
* The specified condition will be added to all {@linkcode Move}s with this attribute,
|
||||
* and moves **will fail upon use** if _at least 1_ of their attached conditions returns `false`.
|
||||
*/
|
||||
getCondition(): MoveCondition | MoveConditionFunc | null {
|
||||
return null;
|
||||
@ -1298,15 +1299,21 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the {@linkcode Move}'s effects are valid to {@linkcode apply}
|
||||
* @virtual
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args Set of unique arguments needed by this attribute
|
||||
* @returns true if basic application of the ability attribute should be possible
|
||||
* Determine whether this {@linkcode MoveAttr}'s effects are able to {@linkcode apply | be applied} to the target.
|
||||
*
|
||||
* Will **NOT** cause the move to fail upon returning `false` (unlike {@linkcode getCondition};
|
||||
* merely that the effect for this attribute will be nullified.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} being targeted by the move, or {@linkcode user} if the move is
|
||||
* {@linkcode selfTarget | self-targeting}
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param _args - Set of unique arguments needed by this attribute
|
||||
* @returns `true` if basic application of this `MoveAttr`s effects should be possible
|
||||
*/
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]) {
|
||||
// TODO: Decouple this check from the `apply` step
|
||||
// TODO: Make non-damaging moves fail by default if none of their attributes can apply
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) {
|
||||
// TODO: These checks seem redundant
|
||||
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
||||
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
||||
@ -1955,19 +1962,17 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class HealAttr extends MoveEffectAttr {
|
||||
/** The percentage of {@linkcode Stat.HP} to heal */
|
||||
private healRatio: number;
|
||||
/** Should an animation be shown? */
|
||||
private showAnim: boolean;
|
||||
|
||||
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) {
|
||||
super(selfTarget === undefined || selfTarget);
|
||||
|
||||
this.healRatio = healRatio || 1;
|
||||
this.showAnim = !!showAnim;
|
||||
constructor(
|
||||
/** The percentage of {@linkcode Stat.HP} to heal. */
|
||||
private healRatio: number,
|
||||
/** Whether to display a healing animation when healing the target; default `false` */
|
||||
private showAnim = false,
|
||||
selfTarget = true
|
||||
) {
|
||||
super(selfTarget);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
||||
return true;
|
||||
}
|
||||
@ -1976,15 +1981,65 @@ export class HealAttr extends MoveEffectAttr {
|
||||
* Creates a new {@linkcode PokemonHealPhase}.
|
||||
* This heals the target and shows the appropriate message.
|
||||
*/
|
||||
addHealPhase(target: Pokemon, healRatio: number) {
|
||||
protected addHealPhase(target: Pokemon, healRatio: number) {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
||||
toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim);
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number {
|
||||
const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10;
|
||||
return Math.round(score / (1 - this.healRatio / 2));
|
||||
}
|
||||
|
||||
// TODO: Change to fail move
|
||||
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||
if (!super.canApply(user, target, _move, _args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const healedPokemon = this.selfTarget ? user : target;
|
||||
if (healedPokemon.isFullHp()) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(healedPokemon),
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to put the user to sleep for a fixed duration, fully heal them and cure their status.
|
||||
* Used for {@linkcode MoveId.REST}.
|
||||
*/
|
||||
export class RestAttr extends HealAttr {
|
||||
private duration: number;
|
||||
|
||||
constructor(duration: number) {
|
||||
super(1, true);
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const wasSet = user.trySetStatus(StatusEffect.SLEEP, user, this.duration, null, true, true,
|
||||
i18next.t("moveTriggers:restBecameHealthy", {
|
||||
pokemonName: getPokemonNameWithAffix(user),
|
||||
}));
|
||||
return wasSet && super.apply(user, target, move, args);
|
||||
}
|
||||
|
||||
override addHealPhase(user: Pokemon): void {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), user.getMaxHp(), null)
|
||||
}
|
||||
|
||||
// TODO: change after HealAttr is changed to fail move
|
||||
override getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) =>
|
||||
super.canApply(user, target, move, [])
|
||||
// Intentionally suppress messages here as we display generic fail msg
|
||||
// TODO: This might have order-of-operation jank
|
||||
&& user.canSetStatus(StatusEffect.SLEEP, true, true, user)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2256,20 +2311,9 @@ export class BoostHealAttr extends HealAttr {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class HealOnAllyAttr extends HealAttr {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args N/A
|
||||
* @returns true if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (user.getAlly() === target) {
|
||||
super.apply(user, target, move, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||
// Don't trigger if not targeting an ally
|
||||
return target === user.getAlly() && super.canApply(user, target, _move, _args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2280,6 +2324,7 @@ export class HealOnAllyAttr extends HealAttr {
|
||||
* @see {@linkcode apply}
|
||||
* @see {@linkcode getUserBenefitScore}
|
||||
*/
|
||||
// TODO: Make Strength Sap its own attribute that extends off of this one
|
||||
export class HitHealAttr extends MoveEffectAttr {
|
||||
private healRatio: number;
|
||||
private healStat: EffectiveStat | null;
|
||||
@ -2530,49 +2575,50 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
|
||||
|
||||
export class StatusEffectAttr extends MoveEffectAttr {
|
||||
public effect: StatusEffect;
|
||||
public turnsRemaining?: number;
|
||||
public overrideStatus: boolean = false;
|
||||
|
||||
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
|
||||
constructor(effect: StatusEffect, selfTarget = false) {
|
||||
super(selfTarget);
|
||||
|
||||
this.effect = effect;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.overrideStatus = overrideStatus;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
||||
if (!statusCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// non-status moves don't play sound effects for failures
|
||||
const quiet = move.category !== MoveCategory.STATUS;
|
||||
if (statusCheck) {
|
||||
const pokemon = this.selfTarget ? user : target;
|
||||
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) {
|
||||
return false;
|
||||
}
|
||||
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
|
||||
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) {
|
||||
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
target.trySetStatus(this.effect, user, undefined, null, false, quiet)
|
||||
) {
|
||||
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
|
||||
const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1);
|
||||
const score = moveChance < 0 ? -10 : Math.floor(moveChance * -0.1);
|
||||
const pokemon = this.selfTarget ? user : target;
|
||||
|
||||
return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
|
||||
return pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to randomly apply one of several statuses to the target.
|
||||
* Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}.
|
||||
*/
|
||||
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||
public effects: StatusEffect[];
|
||||
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
|
||||
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean) {
|
||||
super(effects[0], selfTarget);
|
||||
this.effects = effects;
|
||||
}
|
||||
|
||||
@ -2605,26 +2651,41 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
||||
* @returns - Whether the effect was successfully applied to the target.
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
const statusToApply = user.status?.effect ??
|
||||
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||
|
||||
if (target.status || !statusToApply) {
|
||||
// Bang is justified as condition func returns early if no status is found
|
||||
if (!target.trySetStatus(statusToApply, user)) {
|
||||
return false;
|
||||
} else {
|
||||
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
|
||||
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
|
||||
}
|
||||
|
||||
if (trySetStatus && user.status) {
|
||||
// PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move
|
||||
user.addTag(BattlerTagType.PSYCHO_SHIFT);
|
||||
if (user.status) {
|
||||
// Add tag to user to heal its status effect after the move ends (unless we have comatose);
|
||||
// occurs after move use to ensure correct Synchronize timing
|
||||
user.addTag(BattlerTagType.PSYCHO_SHIFT)
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target) => {
|
||||
if (target.status?.effect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trySetStatus;
|
||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||
}
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||
const statusToApply =
|
||||
user.status?.effect ??
|
||||
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||
|
||||
// TODO: Give this a positive user benefit score
|
||||
return !target.status?.effect && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2684,7 +2745,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
* Used for Incinerate and Knock Off.
|
||||
* Not Implemented Cases: (Same applies for Thief)
|
||||
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item.""
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item."
|
||||
*/
|
||||
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
|
||||
@ -2894,7 +2955,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
*/
|
||||
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
|
||||
super(selfTarget, { lastHitOnly: true });
|
||||
this.effects = [ effects ].flat(1);
|
||||
this.effects = coerceArray(effects)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4421,6 +4482,10 @@ export class SpitUpPowerAttr extends VariablePowerAttr {
|
||||
* Does NOT remove stockpiled stacks.
|
||||
*/
|
||||
export class SwallowHealAttr extends HealAttr {
|
||||
constructor() {
|
||||
super(1)
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
|
||||
@ -7903,7 +7968,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (target.turnData.statStagesIncreased) {
|
||||
target.trySetStatus(this.effect, true, user);
|
||||
target.trySetStatus(this.effect, user);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -8050,11 +8115,11 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return !cancelled.value;
|
||||
};
|
||||
|
||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined;
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
@ -8932,9 +8997,7 @@ export function initMoves() {
|
||||
.attr(MultiHitAttr, MultiHitType._2)
|
||||
.makesContact(false),
|
||||
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
|
||||
.attr(HealAttr, 1, true)
|
||||
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
|
||||
.attr(RestAttr, 3)
|
||||
.triageMove(),
|
||||
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
||||
.attr(FlinchAttr)
|
||||
@ -9280,14 +9343,16 @@ export function initMoves() {
|
||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SpitUpPowerAttr, 100)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
||||
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SwallowHealAttr)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
|
||||
.triageMove(),
|
||||
.triageMove()
|
||||
// TODO: Verify if using Swallow at full HP still consumes stacks or not
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||
@ -9673,14 +9738,8 @@ export function initMoves() {
|
||||
.unimplemented(),
|
||||
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
||||
.attr(PsychoShiftEffectAttr)
|
||||
.condition((user, target, move) => {
|
||||
let statusToApply = user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined;
|
||||
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
|
||||
statusToApply = user.status.effect;
|
||||
}
|
||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||
}
|
||||
),
|
||||
// TODO: Verify status applied if a statused pokemon obtains Comatose (via Transform) and uses Psycho Shift
|
||||
.edgeCase(),
|
||||
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
||||
.makesContact()
|
||||
.attr(LessPPMorePowerAttr),
|
||||
|
@ -243,8 +243,9 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
||||
if (burnable?.length > 0) {
|
||||
const roll = randSeedInt(burnable.length);
|
||||
const chosenPokemon = burnable[roll];
|
||||
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
|
||||
if (chosenPokemon.canSetStatus(StatusEffect.BURN, true)) {
|
||||
// Burn applied
|
||||
chosenPokemon.doSetStatus(StatusEffect.BURN);
|
||||
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
||||
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
||||
queueEncounterMessage(`${namespace}:option.2.target_burned`);
|
||||
|
@ -309,7 +309,7 @@ export function getRandomSpeciesByStarterCost(
|
||||
*/
|
||||
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
||||
pokemon.hp = 0;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
pokemon.updateInfo();
|
||||
queueEncounterMessage(
|
||||
i18next.t("battle:fainted", {
|
||||
|
@ -1,3 +1,5 @@
|
||||
/** Enum representing all non-volatile status effects. */
|
||||
// TODO: Remove StatusEffect.FAINT
|
||||
export enum StatusEffect {
|
||||
NONE,
|
||||
POISON,
|
||||
|
@ -38,6 +38,7 @@ export enum UiMode {
|
||||
UNAVAILABLE,
|
||||
CHALLENGE_SELECT,
|
||||
RENAME_POKEMON,
|
||||
RENAME_RUN,
|
||||
RUN_HISTORY,
|
||||
RUN_INFO,
|
||||
TEST_DIALOGUE,
|
||||
|
@ -235,6 +235,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
public ivs: number[];
|
||||
public nature: Nature;
|
||||
public moveset: PokemonMove[];
|
||||
/**
|
||||
* This Pokemon's current {@link https://m.bulbapedia.bulbagarden.net/wiki/Status_condition#Non-volatile_status | non-volatile status condition},
|
||||
* or `null` if none exist.
|
||||
* @todo Make private
|
||||
*/
|
||||
public status: Status | null;
|
||||
public friendship: number;
|
||||
public metLevel: number;
|
||||
@ -4744,7 +4749,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param reason - The reason for the status application failure -
|
||||
* can be "overlap" (already has same status), "other" (generic fail message)
|
||||
* or a {@linkcode TerrainType} for terrain-based blockages.
|
||||
* Defaults to "other".
|
||||
* Default `"other"`
|
||||
*/
|
||||
queueStatusImmuneMessage(
|
||||
quiet: boolean,
|
||||
@ -4773,15 +4778,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a status effect can be applied to the Pokemon.
|
||||
* Check if a status effect can be applied to this {@linkcode Pokemon}.
|
||||
*
|
||||
* @param effect The {@linkcode StatusEffect} whose applicability is being checked
|
||||
* @param quiet Whether in-battle messages should trigger or not
|
||||
* @param overrideStatus Whether the Pokemon's current status can be overriden
|
||||
* @param sourcePokemon The Pokemon that is setting the status effect
|
||||
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
|
||||
* @param effect - The {@linkcode StatusEffect} whose applicability is being checked
|
||||
* @param quiet - Whether to suppress in-battle messages for status checks; default `false`
|
||||
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`
|
||||
* @param ignoreField - Whether to ignore field effects (weather, terrain, etc.) preventing status application;
|
||||
* default `false`
|
||||
* @returns Whether {@linkcode effect} can be applied to this Pokemon.
|
||||
*/
|
||||
canSetStatus(
|
||||
// TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once
|
||||
// TODO: Make argument order consistent with `trySetStatus`
|
||||
public canSetStatus(
|
||||
effect: StatusEffect,
|
||||
quiet = false,
|
||||
overrideStatus = false,
|
||||
@ -4789,6 +4799,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
ignoreField = false,
|
||||
): boolean {
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
// Status-overriding moves (i.e. Rest) fail if their respective status already exists;
|
||||
// all other moves fail if the target already has _any_ status
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
||||
return false;
|
||||
@ -4801,73 +4813,62 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
const types = this.getTypes(true, true);
|
||||
|
||||
/* Whether the target is immune to the specific status being applied. */
|
||||
let isImmune = false;
|
||||
/** The reason for a potential blockage; default "other" for type-based. */
|
||||
let reason: "other" | Exclude<TerrainType, TerrainType.NONE> = "other";
|
||||
|
||||
switch (effect) {
|
||||
case StatusEffect.POISON:
|
||||
case StatusEffect.TOXIC: {
|
||||
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity
|
||||
const poisonImmunity = types.map(defType => {
|
||||
// Check if the Pokemon is not immune to Poison/Toxic
|
||||
case StatusEffect.TOXIC:
|
||||
// Check for type based immunities and/or Corrosion from the applier.
|
||||
isImmune = types.some(defType => {
|
||||
// only 1 immunity needed to block
|
||||
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
|
||||
// No source (such as from Toxic Spikes) = blocked by default
|
||||
if (!sourcePokemon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cancelImmunity = new BooleanHolder(false);
|
||||
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
||||
if (sourcePokemon) {
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||
pokemon: sourcePokemon,
|
||||
cancelled: cancelImmunity,
|
||||
statusEffect: effect,
|
||||
defenderType: defType,
|
||||
});
|
||||
if (cancelImmunity.value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||
pokemon: sourcePokemon,
|
||||
cancelled: cancelImmunity,
|
||||
statusEffect: effect,
|
||||
defenderType: defType,
|
||||
});
|
||||
return !cancelImmunity.value;
|
||||
});
|
||||
|
||||
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
|
||||
if (poisonImmunity.includes(true)) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case StatusEffect.PARALYSIS:
|
||||
if (this.isOfType(PokemonType.ELECTRIC)) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isOfType(PokemonType.ELECTRIC);
|
||||
break;
|
||||
case StatusEffect.SLEEP:
|
||||
if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
||||
this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.ELECTRIC;
|
||||
reason = TerrainType.ELECTRIC;
|
||||
break;
|
||||
case StatusEffect.FREEZE:
|
||||
if (
|
||||
case StatusEffect.FREEZE: {
|
||||
const weatherType = globalScene.arena.getWeatherType();
|
||||
isImmune =
|
||||
this.isOfType(PokemonType.ICE) ||
|
||||
(!ignoreField &&
|
||||
globalScene?.arena?.weather?.weatherType &&
|
||||
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
|
||||
) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
(!ignoreField && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN));
|
||||
break;
|
||||
}
|
||||
case StatusEffect.BURN:
|
||||
if (this.isOfType(PokemonType.FIRE)) {
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
isImmune = this.isOfType(PokemonType.FIRE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isImmune) {
|
||||
this.queueStatusImmuneMessage(quiet, reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for cancellations from self/ally abilities
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
||||
if (cancelled.value) {
|
||||
@ -4884,14 +4885,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
source: sourcePokemon,
|
||||
});
|
||||
if (cancelled.value) {
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform safeguard checks
|
||||
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
||||
if (!quiet) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -4904,18 +4902,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return true;
|
||||
}
|
||||
|
||||
trySetStatus(
|
||||
effect?: StatusEffect,
|
||||
asPhase = false,
|
||||
/**
|
||||
* Attempt to set this Pokemon's status to the specified condition.
|
||||
* Enqueues a new `ObtainStatusEffectPhase` to trigger animations, etc.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`
|
||||
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses
|
||||
* @param sourceText - The text to show for the source of the status effect, if any; default `null`
|
||||
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||
* @param quiet - Whether to suppress in-battle messages for status checks; default `true`
|
||||
* @param overrideMessage - String containing text to be displayed upon status setting; defaults to normal key for status
|
||||
* and is used exclusively for Rest
|
||||
* @returns Whether the status effect phase was successfully created.
|
||||
* @see {@linkcode doSetStatus} - alternate function that sets status immediately (albeit without condition checks).
|
||||
*/
|
||||
public trySetStatus(
|
||||
effect: StatusEffect,
|
||||
sourcePokemon: Pokemon | null = null,
|
||||
turnsRemaining = 0,
|
||||
sleepTurnsRemaining?: number,
|
||||
sourceText: string | null = null,
|
||||
overrideStatus?: boolean,
|
||||
quiet = true,
|
||||
overrideMessage?: string,
|
||||
): boolean {
|
||||
// TODO: This needs to propagate failure status for status moves
|
||||
if (!effect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
||||
return false;
|
||||
}
|
||||
@ -4935,48 +4951,79 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
if (asPhase) {
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"ObtainStatusEffectPhase",
|
||||
this.getBattlerIndex(),
|
||||
effect,
|
||||
turnsRemaining,
|
||||
sourceText,
|
||||
sourcePokemon,
|
||||
);
|
||||
return true;
|
||||
if (overrideStatus) {
|
||||
this.resetStatus(false);
|
||||
}
|
||||
|
||||
let sleepTurnsRemaining: NumberHolder;
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"ObtainStatusEffectPhase",
|
||||
this.getBattlerIndex(),
|
||||
effect,
|
||||
sourcePokemon,
|
||||
sleepTurnsRemaining,
|
||||
sourceText,
|
||||
overrideMessage,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @remarks
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - {@linkcode StatusEffect.SLEEP}
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* @remarks
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* and is unused for all non-sleep Statuses
|
||||
* @remarks
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
*/
|
||||
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
|
||||
/**
|
||||
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||
* @param effect - The {@linkcode StatusEffect} to set
|
||||
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||
* and is unused for all non-sleep Statuses
|
||||
* @remarks
|
||||
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||
* @todo Make this and all related fields private and change tests to use a field-based helper or similar
|
||||
*/
|
||||
doSetStatus(
|
||||
effect: StatusEffect,
|
||||
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4),
|
||||
): void {
|
||||
if (effect === StatusEffect.SLEEP) {
|
||||
sleepTurnsRemaining = new NumberHolder(this.randBattleSeedIntRange(2, 4));
|
||||
|
||||
this.setFrameRate(4);
|
||||
|
||||
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
|
||||
const invulnerableTags = [
|
||||
// If the user is semi-invulnerable when put asleep (such as due to Yawm),
|
||||
// remove their invulnerability and cancel the upcoming move from the queue
|
||||
const invulnTagTypes = [
|
||||
BattlerTagType.FLYING,
|
||||
BattlerTagType.UNDERGROUND,
|
||||
BattlerTagType.UNDERWATER,
|
||||
BattlerTagType.HIDDEN,
|
||||
BattlerTagType.FLYING,
|
||||
];
|
||||
|
||||
const tag = invulnerableTags.find(t => this.getTag(t));
|
||||
|
||||
if (tag) {
|
||||
this.removeTag(tag);
|
||||
this.getMoveQueue().pop();
|
||||
if (this.findTag(t => invulnTagTypes.includes(t.tagType))) {
|
||||
this.findAndRemoveTags(t => invulnTagTypes.includes(t.tagType));
|
||||
this.getMoveQueue().shift();
|
||||
}
|
||||
}
|
||||
|
||||
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
|
||||
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
|
||||
|
||||
return true;
|
||||
this.status = new Status(effect, 0, sleepTurnsRemaining);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1733,12 +1733,12 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to inflicts the holder with the associated {@linkcode StatusEffect}.
|
||||
* @param pokemon {@linkcode Pokemon} that holds the held item
|
||||
* Attempt to inflict the holder with the associated {@linkcode StatusEffect}.
|
||||
* @param pokemon - The {@linkcode Pokemon} holding the item
|
||||
* @returns `true` if the status effect was applied successfully
|
||||
*/
|
||||
override apply(pokemon: Pokemon): boolean {
|
||||
return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name);
|
||||
return pokemon.trySetStatus(this.effect, pokemon, undefined, this.type.name);
|
||||
}
|
||||
|
||||
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
||||
@ -3605,7 +3605,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi
|
||||
*/
|
||||
override apply(enemyPokemon: Pokemon): boolean {
|
||||
if (randSeedFloat() <= this.chance * this.getStackCount()) {
|
||||
return enemyPokemon.trySetStatus(this.effect, true);
|
||||
return enemyPokemon.trySetStatus(this.effect);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -268,7 +268,7 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||
const removePokemon = () => {
|
||||
globalScene.addFaintedEnemyScore(pokemon);
|
||||
pokemon.hp = 0;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
globalScene.clearEnemyHeldItemModifiers();
|
||||
pokemon.leaveField(true, true, true);
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ export class AttemptRunPhase extends FieldPhase {
|
||||
enemyField.forEach(enemyPokemon => {
|
||||
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
||||
enemyPokemon.hp = 0;
|
||||
enemyPokemon.trySetStatus(StatusEffect.FAINT);
|
||||
enemyPokemon.doSetStatus(StatusEffect.FAINT);
|
||||
});
|
||||
|
||||
globalScene.phaseManager.pushNew("BattleEndPhase", false);
|
||||
|
@ -205,7 +205,7 @@ export class FaintPhase extends PokemonPhase {
|
||||
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
||||
|
||||
pokemon.y -= 150;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||
if (pokemon.isPlayer()) {
|
||||
globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
|
||||
} else {
|
||||
|
@ -269,8 +269,8 @@ export class MovePhase extends BattlePhase {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
this.pokemon.resetStatus();
|
||||
this.pokemon.updateInfo();
|
||||
// cannot use `asPhase=true` as it will cause status to be reset _after_ move condition checks fire
|
||||
this.pokemon.resetStatus(false, false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,71 +3,64 @@ import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { CommonBattleAnim } from "#data/battle-anims";
|
||||
import { SpeciesFormChangeStatusEffectTrigger } from "#data/form-change-triggers";
|
||||
import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#data/status-effect";
|
||||
import { getStatusEffectObtainText } from "#data/status-effect";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
import { isNullOrUndefined } from "#utils/common";
|
||||
|
||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
public readonly phaseName = "ObtainStatusEffectPhase";
|
||||
private statusEffect?: StatusEffect;
|
||||
private turnsRemaining?: number;
|
||||
private sourceText?: string | null;
|
||||
private sourcePokemon?: Pokemon | null;
|
||||
|
||||
/**
|
||||
* @param battlerIndex - The {@linkcode BattlerIndex} of the Pokemon obtaining the status effect.
|
||||
* @param statusEffect - The {@linkcode StatusEffect} being applied.
|
||||
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`.
|
||||
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses.
|
||||
* @param sourceText - The text to show for the source of the status effect, if any; default `null`.
|
||||
* @param statusMessage - A string containing text to be displayed upon status setting;
|
||||
* defaults to normal key for status if empty or omitted.
|
||||
*/
|
||||
constructor(
|
||||
battlerIndex: BattlerIndex,
|
||||
statusEffect?: StatusEffect,
|
||||
turnsRemaining?: number,
|
||||
sourceText?: string | null,
|
||||
sourcePokemon?: Pokemon | null,
|
||||
private statusEffect: StatusEffect,
|
||||
private sourcePokemon: Pokemon | null = null,
|
||||
private sleepTurnsRemaining?: number,
|
||||
sourceText: string | null = null, // TODO: This should take `undefined` instead of `null`
|
||||
private statusMessage = "",
|
||||
) {
|
||||
super(battlerIndex);
|
||||
|
||||
this.statusEffect = statusEffect;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.sourceText = sourceText;
|
||||
this.sourcePokemon = sourcePokemon;
|
||||
this.statusMessage ||= getStatusEffectObtainText(
|
||||
statusEffect,
|
||||
getPokemonNameWithAffix(this.getPokemon()),
|
||||
sourceText ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
const pokemon = this.getPokemon();
|
||||
if (pokemon && !pokemon.status) {
|
||||
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
||||
if (this.turnsRemaining) {
|
||||
pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
|
||||
}
|
||||
pokemon.updateInfo(true);
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectObtainText(
|
||||
this.statusEffect,
|
||||
getPokemonNameWithAffix(pokemon),
|
||||
this.sourceText ?? undefined,
|
||||
),
|
||||
);
|
||||
if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) {
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
|
||||
// If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
applyAbAttrs("PostSetStatusAbAttr", {
|
||||
pokemon,
|
||||
effect: this.statusEffect,
|
||||
sourcePokemon: this.sourcePokemon ?? undefined,
|
||||
});
|
||||
}
|
||||
this.end();
|
||||
|
||||
pokemon.doSetStatus(this.statusEffect, this.sleepTurnsRemaining);
|
||||
pokemon.updateInfo(true);
|
||||
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(false, () => {
|
||||
globalScene.phaseManager.queueMessage(this.statusMessage);
|
||||
if (this.statusEffect && this.statusEffect !== StatusEffect.FAINT) {
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
|
||||
// If the status was applied from a move, ensure abilities are not ignored for follow-up triggers.
|
||||
// TODO: Ensure this isn't breaking any other phases unshifted afterwards
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
applyAbAttrs("PostSetStatusAbAttr", {
|
||||
pokemon,
|
||||
effect: this.statusEffect,
|
||||
sourcePokemon: this.sourcePokemon ?? undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (pokemon.status?.effect === this.statusEffect) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)),
|
||||
);
|
||||
}
|
||||
this.end();
|
||||
this.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { HealAchv } from "#system/achv";
|
||||
import { NumberHolder } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
|
||||
// TODO: Refactor this - it has far too many arguments
|
||||
export class PokemonHealPhase extends CommonAnimPhase {
|
||||
public readonly phaseName = "PokemonHealPhase";
|
||||
private hpHealed: number;
|
||||
@ -28,7 +29,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
||||
battlerIndex: BattlerIndex,
|
||||
hpHealed: number,
|
||||
message: string | null,
|
||||
showFullHpMessage: boolean,
|
||||
showFullHpMessage = true,
|
||||
skipAnim = false,
|
||||
revive = false,
|
||||
healStatus = false,
|
||||
@ -72,6 +73,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
||||
this.message = null;
|
||||
return super.end();
|
||||
}
|
||||
|
||||
if (healOrDamage) {
|
||||
const hpRestoreMultiplier = new NumberHolder(1);
|
||||
if (!this.revive) {
|
||||
|
@ -127,6 +127,7 @@ export interface SessionSaveData {
|
||||
battleType: BattleType;
|
||||
trainer: TrainerData;
|
||||
gameVersion: string;
|
||||
runNameText: string;
|
||||
timestamp: number;
|
||||
challenges: ChallengeData[];
|
||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||
@ -979,6 +980,54 @@ export class GameData {
|
||||
});
|
||||
}
|
||||
|
||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
if (slotId < 0) {
|
||||
return resolve(false);
|
||||
}
|
||||
const sessionData: SessionSaveData | null = await this.getSession(slotId);
|
||||
|
||||
if (!sessionData) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
if (newName === "") {
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
sessionData.runNameText = newName;
|
||||
const updatedDataStr = JSON.stringify(sessionData);
|
||||
const encrypted = encrypt(updatedDataStr, bypassLogin);
|
||||
const secretId = this.secretId;
|
||||
const trainerId = this.trainerId;
|
||||
|
||||
if (bypassLogin) {
|
||||
localStorage.setItem(
|
||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||
encrypt(updatedDataStr, bypassLogin),
|
||||
);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
pokerogueApi.savedata.session
|
||||
.update({ slot: slotId, trainerId, secretId, clientSessionId }, encrypted)
|
||||
.then(error => {
|
||||
if (error) {
|
||||
console.error("Failed to update session name:", error);
|
||||
resolve(false);
|
||||
} else {
|
||||
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted);
|
||||
updateUserInfo().then(success => {
|
||||
if (success !== null && !success) {
|
||||
return resolve(false);
|
||||
}
|
||||
});
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
54
src/ui/rename-run-ui-handler.ts
Normal file
54
src/ui/rename-run-ui-handler.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import i18next from "i18next";
|
||||
import type { InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import type { ModalConfig } from "./modal-ui-handler";
|
||||
|
||||
export class RenameRunFormUiHandler extends FormModalUiHandler {
|
||||
getModalTitle(_config?: ModalConfig): string {
|
||||
return i18next.t("menu:renamerun");
|
||||
}
|
||||
|
||||
getWidth(_config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
|
||||
getMargin(_config?: ModalConfig): [number, number, number, number] {
|
||||
return [0, 0, 48, 0];
|
||||
}
|
||||
|
||||
getButtonLabels(_config?: ModalConfig): string[] {
|
||||
return [i18next.t("menu:rename"), i18next.t("menu:cancel")];
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
const colonIndex = error?.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
error = error.slice(0, colonIndex);
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [{ label: i18next.t("menu:runName") }];
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
if (!super.show(args)) {
|
||||
return false;
|
||||
}
|
||||
if (this.inputs?.length) {
|
||||
this.inputs.forEach(input => {
|
||||
input.text = "";
|
||||
});
|
||||
}
|
||||
const config = args[0] as ModalConfig;
|
||||
this.submitAction = _ => {
|
||||
this.sanitizeInputs();
|
||||
const sanitizedName = btoa(encodeURIComponent(this.inputs[0].text));
|
||||
config.buttonActions[0](sanitizedName);
|
||||
return true;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
@ -207,6 +207,10 @@ export class RunInfoUiHandler extends UiHandler {
|
||||
headerText.setOrigin(0, 0);
|
||||
headerText.setPositionRelative(headerBg, 8, 4);
|
||||
this.runContainer.add(headerText);
|
||||
const runName = addTextObject(0, 0, this.runInfo.runNameText, TextStyle.WINDOW);
|
||||
runName.setOrigin(0, 0);
|
||||
runName.setPositionRelative(headerBg, 60, 4);
|
||||
this.runContainer.add(runName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
||||
import * as Modifier from "#modifiers/modifier";
|
||||
import type { SessionSaveData } from "#system/game-data";
|
||||
import type { PokemonData } from "#system/pokemon-data";
|
||||
import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler";
|
||||
import { MessageUiHandler } from "#ui/message-ui-handler";
|
||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||
import { addTextObject } from "#ui/text";
|
||||
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
|
||||
import i18next from "i18next";
|
||||
|
||||
const SESSION_SLOTS_COUNT = 5;
|
||||
const SLOTS_ON_SCREEN = 3;
|
||||
const SLOTS_ON_SCREEN = 2;
|
||||
|
||||
export enum SaveSlotUiMode {
|
||||
LOAD,
|
||||
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
private uiMode: SaveSlotUiMode;
|
||||
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
||||
protected manageDataConfig: OptionSelectConfig;
|
||||
|
||||
private scrollCursor = 0;
|
||||
|
||||
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
const manageDataOptions: any[] = [];
|
||||
|
||||
let success = false;
|
||||
let error = false;
|
||||
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
if (button === Button.ACTION) {
|
||||
const cursor = this.cursor + this.scrollCursor;
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !this.sessionSlots[cursor].hasData) {
|
||||
const sessionSlot = this.sessionSlots[cursor];
|
||||
if (this.uiMode === SaveSlotUiMode.LOAD && !sessionSlot.hasData) {
|
||||
error = true;
|
||||
} else {
|
||||
switch (this.uiMode) {
|
||||
case SaveSlotUiMode.LOAD:
|
||||
this.saveSlotSelectCallback = null;
|
||||
originalCallback?.(cursor);
|
||||
if (!sessionSlot.malformed) {
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menu:loadGame"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
originalCallback?.(cursor);
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:renameRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.setOverlayMode(
|
||||
UiMode.RENAME_RUN,
|
||||
{
|
||||
buttonActions: [
|
||||
(sanitizedName: string) => {
|
||||
const name = decodeURIComponent(atob(sanitizedName));
|
||||
globalScene.gameData.renameSession(cursor, name).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
},
|
||||
],
|
||||
},
|
||||
"",
|
||||
);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.manageDataConfig = {
|
||||
xOffset: 0,
|
||||
yOffset: 48,
|
||||
options: manageDataOptions,
|
||||
maxOptions: 4,
|
||||
};
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("saveSlotSelectUiHandler:deleteRun"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
ui.showText(i18next.t("saveSlotSelectUiHandler:deleteData"), null, () => {
|
||||
ui.setOverlayMode(
|
||||
UiMode.CONFIRM,
|
||||
() => {
|
||||
globalScene.gameData.tryClearSession(cursor).then(response => {
|
||||
if (response[0] === false) {
|
||||
globalScene.reset(true);
|
||||
} else {
|
||||
this.clearSessionSlots();
|
||||
this.cursorObj = null;
|
||||
this.populateSessionSlots();
|
||||
this.setScrollCursor(0);
|
||||
this.setCursor(0);
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
ui.showText("", 0);
|
||||
},
|
||||
false,
|
||||
0,
|
||||
19,
|
||||
import.meta.env.DEV ? 300 : 2000,
|
||||
);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
keepOpen: false,
|
||||
});
|
||||
|
||||
manageDataOptions.push({
|
||||
label: i18next.t("menuUiHandler:cancel"),
|
||||
handler: () => {
|
||||
globalScene.ui.revertMode();
|
||||
return true;
|
||||
},
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
ui.setOverlayMode(UiMode.MENU_OPTION_SELECT, this.manageDataConfig);
|
||||
break;
|
||||
|
||||
case SaveSlotUiMode.SAVE: {
|
||||
const saveAndCallback = () => {
|
||||
const originalCallback = this.saveSlotSelectCallback;
|
||||
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} else {
|
||||
this.saveSlotSelectCallback = null;
|
||||
ui.showText("", 0);
|
||||
originalCallback?.(-1);
|
||||
success = true;
|
||||
}
|
||||
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.cursorObj = globalScene.add.container(0, 0);
|
||||
const cursorBox = globalScene.add.nineslice(
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
"select_cursor_highlight_thick",
|
||||
undefined,
|
||||
296,
|
||||
44,
|
||||
294,
|
||||
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
6,
|
||||
);
|
||||
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
||||
rightArrow.setPosition(160, 0);
|
||||
rightArrow.setPosition(160, 15);
|
||||
rightArrow.setName("rightArrow");
|
||||
this.cursorObj.add([cursorBox, rightArrow]);
|
||||
this.sessionSlotsContainer.add(this.cursorObj);
|
||||
}
|
||||
const cursorPosition = cursor + this.scrollCursor;
|
||||
const cursorIncrement = cursorPosition * 56;
|
||||
const cursorIncrement = cursorPosition * 76;
|
||||
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
|
||||
const hasData = this.sessionSlots[cursorPosition].hasData;
|
||||
const session = this.sessionSlots[cursorPosition];
|
||||
const hasData = session.hasData && !session.malformed;
|
||||
// If the session slot lacks session data, it does not move from its default, central position.
|
||||
// Only session slots with session data will move leftwards and have a visible arrow.
|
||||
if (!hasData) {
|
||||
this.cursorObj.setPosition(151, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(151, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
||||
} else {
|
||||
this.cursorObj.setPosition(145, 26 + cursorIncrement);
|
||||
this.cursorObj.setPosition(145, 20 + cursorIncrement);
|
||||
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
||||
}
|
||||
this.setArrowVisibility(hasData);
|
||||
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
revertSessionSlot(slotIndex: number): void {
|
||||
const sessionSlot = this.sessionSlots[slotIndex];
|
||||
if (sessionSlot) {
|
||||
sessionSlot.setPosition(0, slotIndex * 56);
|
||||
const valueHeight = 76;
|
||||
sessionSlot.setPosition(0, slotIndex * valueHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +448,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
this.setCursor(this.cursor, prevSlotIndex);
|
||||
globalScene.tweens.add({
|
||||
targets: this.sessionSlotsContainer,
|
||||
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
|
||||
y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
|
||||
duration: fixedInt(325),
|
||||
ease: "Sine.easeInOut",
|
||||
});
|
||||
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
||||
class SessionSlot extends Phaser.GameObjects.Container {
|
||||
public slotId: number;
|
||||
public hasData: boolean;
|
||||
/** Indicates the save slot ran into an error while being loaded */
|
||||
public malformed: boolean;
|
||||
private slotWindow: Phaser.GameObjects.NineSlice;
|
||||
private loadingLabel: Phaser.GameObjects.Text;
|
||||
|
||||
public saveData: SessionSaveData;
|
||||
|
||||
constructor(slotId: number) {
|
||||
super(globalScene, 0, slotId * 56);
|
||||
super(globalScene, 0, slotId * 76);
|
||||
|
||||
this.slotId = slotId;
|
||||
|
||||
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
setup() {
|
||||
const slotWindow = addWindow(0, 0, 304, 52);
|
||||
this.add(slotWindow);
|
||||
this.slotWindow = addWindow(0, 0, 304, 70);
|
||||
this.add(this.slotWindow);
|
||||
|
||||
this.loadingLabel = addTextObject(152, 26, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel = addTextObject(152, 33, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW);
|
||||
this.loadingLabel.setOrigin(0.5, 0.5);
|
||||
this.add(this.loadingLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a name for sessions that don't have a name yet.
|
||||
* @param data - The {@linkcode SessionSaveData} being checked
|
||||
* @returns The default name for the given data.
|
||||
*/
|
||||
decideFallback(data: SessionSaveData): string {
|
||||
let fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
switch (data.gameMode) {
|
||||
case GameModes.CLASSIC:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.classicSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.ENDLESS:
|
||||
case GameModes.SPLICED_ENDLESS:
|
||||
fallbackName += ` (${globalScene.gameData.gameStats.endlessSessionsPlayed + 1})`;
|
||||
break;
|
||||
case GameModes.DAILY: {
|
||||
const runDay = new Date(data.timestamp).toLocaleDateString();
|
||||
fallbackName += ` (${runDay})`;
|
||||
break;
|
||||
}
|
||||
case GameModes.CHALLENGE: {
|
||||
const activeChallenges = data.challenges.filter(c => c.value !== 0);
|
||||
if (activeChallenges.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
fallbackName = "";
|
||||
for (const challenge of activeChallenges.slice(0, 3)) {
|
||||
if (fallbackName !== "") {
|
||||
fallbackName += ", ";
|
||||
}
|
||||
fallbackName += challenge.toChallenge().getName();
|
||||
}
|
||||
|
||||
if (activeChallenges.length > 3) {
|
||||
fallbackName += ", ...";
|
||||
} else if (fallbackName === "") {
|
||||
// Something went wrong when retrieving the names of the active challenges,
|
||||
// so fall back to just naming the run "Challenge"
|
||||
fallbackName = `${GameMode.getModeName(data.gameMode)}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
async setupWithData(data: SessionSaveData) {
|
||||
const hasName = data?.runNameText;
|
||||
this.remove(this.loadingLabel, true);
|
||||
if (hasName) {
|
||||
const nameLabel = addTextObject(8, 5, data.runNameText, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
} else {
|
||||
const fallbackName = this.decideFallback(data);
|
||||
await globalScene.gameData.renameSession(this.slotId, fallbackName);
|
||||
const nameLabel = addTextObject(8, 5, fallbackName, TextStyle.WINDOW);
|
||||
this.add(nameLabel);
|
||||
}
|
||||
|
||||
const gameModeLabel = addTextObject(
|
||||
8,
|
||||
5,
|
||||
19,
|
||||
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
||||
TextStyle.WINDOW,
|
||||
);
|
||||
this.add(gameModeLabel);
|
||||
|
||||
const timestampLabel = addTextObject(8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
const timestampLabel = addTextObject(8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW);
|
||||
this.add(timestampLabel);
|
||||
|
||||
const playTimeLabel = addTextObject(8, 33, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
const playTimeLabel = addTextObject(8, 47, getPlayTimeString(data.playTime), TextStyle.WINDOW);
|
||||
this.add(playTimeLabel);
|
||||
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 4);
|
||||
const pokemonIconsContainer = globalScene.add.container(144, 16);
|
||||
data.party.forEach((p: PokemonData, i: number) => {
|
||||
const iconContainer = globalScene.add.container(26 * i, 0);
|
||||
iconContainer.setScale(0.75);
|
||||
@ -441,7 +608,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
this.add(pokemonIconsContainer);
|
||||
|
||||
const modifierIconsContainer = globalScene.add.container(148, 30);
|
||||
const modifierIconsContainer = globalScene.add.container(148, 38);
|
||||
modifierIconsContainer.setScale(0.5);
|
||||
let visibleModifierIndex = 0;
|
||||
for (const m of data.modifiers) {
|
||||
@ -464,22 +631,32 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
||||
|
||||
load(): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
globalScene.gameData.getSession(this.slotId).then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
if (!sessionData) {
|
||||
this.hasData = false;
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.hasData = true;
|
||||
this.saveData = sessionData;
|
||||
await this.setupWithData(sessionData);
|
||||
resolve(true);
|
||||
});
|
||||
globalScene.gameData
|
||||
.getSession(this.slotId)
|
||||
.then(async sessionData => {
|
||||
// Ignore the results if the view was exited
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
this.hasData = !!sessionData;
|
||||
if (!sessionData) {
|
||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
this.saveData = sessionData;
|
||||
resolve(true);
|
||||
})
|
||||
.catch(e => {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
console.warn(`Failed to load session slot #${this.slotId}:`, e);
|
||||
this.loadingLabel.setText(i18next.t("menu:failedToLoadSession"));
|
||||
this.hasData = true;
|
||||
this.malformed = true;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
|
||||
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
||||
import { executeIf } from "#utils/common";
|
||||
import i18next from "i18next";
|
||||
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
|
||||
|
||||
const transitionModes = [
|
||||
UiMode.SAVE_SLOT,
|
||||
@ -98,6 +99,7 @@ const noTransitionModes = [
|
||||
UiMode.SESSION_RELOAD,
|
||||
UiMode.UNAVAILABLE,
|
||||
UiMode.RENAME_POKEMON,
|
||||
UiMode.RENAME_RUN,
|
||||
UiMode.TEST_DIALOGUE,
|
||||
UiMode.AUTO_COMPLETE,
|
||||
UiMode.ADMIN,
|
||||
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
|
||||
new UnavailableModalUiHandler(),
|
||||
new GameChallengesUiHandler(),
|
||||
new RenameFormUiHandler(),
|
||||
new RenameRunFormUiHandler(),
|
||||
new RunHistoryUiHandler(),
|
||||
new RunInfoUiHandler(),
|
||||
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -22,25 +23,66 @@ describe("Abilities - Corrosion", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.GRIMER)
|
||||
.enemyAbility(AbilityId.CORROSION)
|
||||
.enemyMoveset(MoveId.TOXIC);
|
||||
.ability(AbilityId.CORROSION)
|
||||
.enemyAbility(AbilityId.NO_GUARD)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => {
|
||||
game.override.ability(AbilityId.SYNCHRONIZE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
it.each<{ name: string; species: SpeciesId }>([
|
||||
{ name: "Poison", species: SpeciesId.GRIMER },
|
||||
{ name: "Steel", species: SpeciesId.KLINK },
|
||||
])("should grant the user the ability to poison $name-type opponents", async ({ species }) => {
|
||||
game.override.enemySpecies(species);
|
||||
await game.classicMode.startBattle([SpeciesId.SALANDIT]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(playerPokemon!.status).toBeUndefined();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.status?.effect).toBeUndefined();
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(playerPokemon!.status).toBeDefined();
|
||||
expect(enemyPokemon!.status).toBeUndefined();
|
||||
game.move.use(MoveId.POISON_GAS);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.POISON);
|
||||
});
|
||||
|
||||
it("should not affect Toxic Spikes", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SALANDIT]);
|
||||
|
||||
game.move.use(MoveId.TOXIC_SPIKES);
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
expect(enemyPokemon.status).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not affect an opponent's Synchronize ability", async () => {
|
||||
game.override.enemyAbility(AbilityId.SYNCHRONIZE);
|
||||
await game.classicMode.startBattle([SpeciesId.ARBOK]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
expect(enemyPokemon.status?.effect).toBeUndefined();
|
||||
|
||||
game.move.use(MoveId.TOXIC);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
expect(playerPokemon.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should affect the user's held Toxic Orb", async () => {
|
||||
game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]);
|
||||
await game.classicMode.startBattle([SpeciesId.SALAZZLE]);
|
||||
|
||||
const salazzle = game.field.getPlayerPokemon();
|
||||
expect(salazzle.status?.effect).toBeUndefined();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(salazzle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
});
|
||||
});
|
||||
|
@ -49,6 +49,7 @@ describe("Abilities - Healer", () => {
|
||||
const user = game.field.getPlayerPokemon();
|
||||
// Only want one magikarp to have the ability
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
// faint the ally
|
||||
game.move.select(MoveId.LUNAR_DANCE, 1);
|
||||
@ -62,9 +63,10 @@ describe("Abilities - Healer", () => {
|
||||
it("should heal the status of an ally if the ally has a status", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
const [user, ally] = game.scene.getPlayerField();
|
||||
|
||||
// Only want one magikarp to have the ability.
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true);
|
||||
ally.doSetStatus(StatusEffect.BURN);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
|
||||
@ -80,7 +82,7 @@ describe("Abilities - Healer", () => {
|
||||
const [user, ally] = game.scene.getPlayerField();
|
||||
// Only want one magikarp to have the ability.
|
||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||
expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true);
|
||||
ally.doSetStatus(StatusEffect.BURN);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
@ -79,9 +79,9 @@ describe("Abilities - Infiltrator", () => {
|
||||
|
||||
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, MoveId.NONE, enemy.id, ArenaTagSide.ENEMY, true);
|
||||
|
||||
game.move.select(MoveId.SPORE);
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(player.waveData.abilitiesApplied).toContain(AbilityId.INFILTRATOR);
|
||||
});
|
||||
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Insomnia", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove sleep when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.INSOMNIA)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.SLEEP);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Limber", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove paralysis when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.LIMBER)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.PARALYSIS);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Magma Armor", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove freeze when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.MAGMA_ARMOR)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.FREEZE);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.FREEZE);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
95
test/abilities/status-immunity-ab-attrs.test.ts
Normal file
95
test/abilities/status-immunity-ab-attrs.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { StatusEffectAttr } from "#moves/move";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { toTitleCase } from "#utils/strings";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe.each<{ name: string; ability: AbilityId; status: StatusEffect }>([
|
||||
{ name: "Vital Spirit", ability: AbilityId.VITAL_SPIRIT, status: StatusEffect.SLEEP },
|
||||
{ name: "Insomnia", ability: AbilityId.INSOMNIA, status: StatusEffect.SLEEP },
|
||||
{ name: "Immunity", ability: AbilityId.IMMUNITY, status: StatusEffect.POISON },
|
||||
{ name: "Magma Armor", ability: AbilityId.MAGMA_ARMOR, status: StatusEffect.FREEZE },
|
||||
{ name: "Limber", ability: AbilityId.LIMBER, status: StatusEffect.PARALYSIS },
|
||||
{ name: "Thermal Exchange", ability: AbilityId.THERMAL_EXCHANGE, status: StatusEffect.BURN },
|
||||
{ name: "Water Veil", ability: AbilityId.WATER_VEIL, status: StatusEffect.BURN },
|
||||
{ name: "Water Bubble", ability: AbilityId.WATER_BUBBLE, status: StatusEffect.BURN },
|
||||
])("Abilities - $name", ({ ability, status }) => {
|
||||
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")
|
||||
.criticalHits(false)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(ability)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
|
||||
// Mock Lumina Crash and Spore to be our status-inflicting moves of choice
|
||||
vi.spyOn(allMoves[MoveId.LUMINA_CRASH], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]);
|
||||
vi.spyOn(allMoves[MoveId.SPORE], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]);
|
||||
});
|
||||
|
||||
const statusStr = toTitleCase(StatusEffect[status]);
|
||||
|
||||
it(`should prevent application of ${statusStr} without failing damaging moves`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const karp = game.field.getEnemyPokemon();
|
||||
expect(karp.status?.effect).toBeUndefined();
|
||||
expect(karp.canSetStatus(status)).toBe(false);
|
||||
|
||||
game.move.use(MoveId.LUMINA_CRASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(karp.status?.effect).toBeUndefined();
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it(`should cure ${statusStr} upon being gained`, async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
feebas.doSetStatus(status);
|
||||
expect(feebas.status?.effect).toBe(status);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SKILL_SWAP);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(feebas.status?.effect).toBeUndefined();
|
||||
});
|
||||
|
||||
// TODO: This does not propagate failures currently
|
||||
it.todo(
|
||||
`should cause status moves inflicting ${statusStr} to count as failed if no other effects can be applied`,
|
||||
async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.use(MoveId.SPORE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const karp = game.field.getEnemyPokemon();
|
||||
expect(karp.status?.effect).toBeUndefined();
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
},
|
||||
);
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Thermal Exchange", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove burn when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.BURN);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Vital Spirit", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove sleep when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.INSOMNIA)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.SLEEP);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Water Bubble", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove burn when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.BURN);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Water Veil", () => {
|
||||
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([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove burn when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.BURN);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
});
|
||||
});
|
@ -6,7 +6,7 @@ import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Immunity", () => {
|
||||
describe("Spec - Pokemon Functions", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
@ -23,29 +23,29 @@ describe("Abilities - Immunity", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should remove poison when gained", async () => {
|
||||
game.override
|
||||
.ability(AbilityId.IMMUNITY)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.moveset(MoveId.SKILL_SWAP)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
enemy?.trySetStatus(StatusEffect.POISON);
|
||||
expect(enemy?.status?.effect).toBe(StatusEffect.POISON);
|
||||
describe("doSetStatus", () => {
|
||||
it("should change the Pokemon's status, ignoring feasibility checks", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.ACCELGOR]);
|
||||
|
||||
game.move.select(MoveId.SKILL_SWAP);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
expect(enemy?.status).toBeNull();
|
||||
expect(player.status?.effect).toBeUndefined();
|
||||
player.doSetStatus(StatusEffect.BURN);
|
||||
expect(player.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
expect(player.canSetStatus(StatusEffect.SLEEP)).toBe(false);
|
||||
player.doSetStatus(StatusEffect.SLEEP, 5);
|
||||
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(player.status?.sleepTurnsRemaining).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
@ -25,15 +25,6 @@ describe("Spec - Pokemon", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
});
|
||||
|
||||
it("should not crash when trying to set status of undefined", async () => {
|
||||
await game.classicMode.runToSummon([SpeciesId.ABRA]);
|
||||
|
||||
const pkm = game.field.getPlayerPokemon();
|
||||
expect(pkm).toBeDefined();
|
||||
|
||||
expect(pkm.trySetStatus(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
describe("Add To Party", () => {
|
||||
let scene: BattleScene;
|
||||
|
||||
|
@ -73,7 +73,7 @@ describe("Moves - Beat Up", () => {
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
|
||||
game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN);
|
||||
game.scene.getPlayerParty()[1].doSetStatus(StatusEffect.BURN);
|
||||
|
||||
game.move.select(MoveId.BEAT_UP);
|
||||
|
||||
|
@ -44,7 +44,7 @@ describe("Moves - Fusion Flare", () => {
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
|
||||
// Inflict freeze quietly and check if it was properly inflicted
|
||||
partyMember.trySetStatus(StatusEffect.FREEZE, false);
|
||||
partyMember.doSetStatus(StatusEffect.FREEZE);
|
||||
expect(partyMember.status!.effect).toBe(StatusEffect.FREEZE);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
146
test/moves/rest.test.ts
Normal file
146
test/moves/rest.test.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Move - Rest", () => {
|
||||
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
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.EKANS)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should fully heal the user, cure its prior status and put it to sleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.POISON);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.POISON);
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.hp).toBe(snorlax.getMaxHp());
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("should always last 3 turns", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
|
||||
// Cf https://bulbapedia.bulbagarden.net/wiki/Rest_(move):
|
||||
// > The user is unable to use MoveId while asleep for 2 turns after the turn when Rest is used.
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(snorlax.status?.sleepTurnsRemaining).toBe(3);
|
||||
|
||||
game.move.use(MoveId.SWORDS_DANCE);
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.status?.sleepTurnsRemaining).toBe(2);
|
||||
|
||||
game.move.use(MoveId.SWORDS_DANCE);
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.status?.sleepTurnsRemaining).toBe(1);
|
||||
|
||||
game.move.use(MoveId.SWORDS_DANCE);
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.status?.effect).toBeUndefined();
|
||||
expect(snorlax.getStatStage(Stat.ATK)).toBe(2);
|
||||
});
|
||||
|
||||
it("should preserve non-volatile status conditions", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
snorlax.addTag(BattlerTagType.CONFUSED, 999);
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
||||
});
|
||||
|
||||
it.each<{ name: string; status?: StatusEffect; ability?: AbilityId; dmg?: number }>([
|
||||
{ name: "is at full HP", dmg: 0 },
|
||||
{ name: "is grounded on Electric Terrain", ability: AbilityId.ELECTRIC_SURGE },
|
||||
{ name: "is grounded on Misty Terrain", ability: AbilityId.MISTY_SURGE },
|
||||
{ name: "has Comatose", ability: AbilityId.COMATOSE },
|
||||
])("should fail if the user $name", async ({ status = StatusEffect.NONE, ability = AbilityId.NONE, dmg = 1 }) => {
|
||||
game.override.ability(ability).statusEffect(status);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
|
||||
snorlax.hp = snorlax.getMaxHp() - dmg;
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if called while already asleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.SLEEP).moveset([MoveId.REST, MoveId.SLEEP_TALK]);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
|
||||
// Need to use sleep talk here since you normally can't move while asleep
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(snorlax.isFullHp()).toBe(false);
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(snorlax.getLastXMoves(-1).map(tm => tm.result)).toEqual([MoveResult.FAIL, MoveResult.SUCCESS]);
|
||||
});
|
||||
|
||||
it("should succeed if called the same turn as the user wakes", async () => {
|
||||
game.override.statusEffect(StatusEffect.SLEEP);
|
||||
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||
|
||||
const snorlax = game.field.getPlayerPokemon();
|
||||
snorlax.hp = 1;
|
||||
|
||||
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
snorlax.status!.sleepTurnsRemaining = 1;
|
||||
|
||||
game.move.use(MoveId.REST);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(snorlax.status!.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(snorlax.isFullHp()).toBe(true);
|
||||
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(snorlax.status!.sleepTurnsRemaining).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
@ -31,13 +32,36 @@ describe("Moves - Sleep Talk", () => {
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyAbility(AbilityId.NO_GUARD)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should fail when the user is not asleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.NONE);
|
||||
it("should call a random valid move if the user is asleep", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(feebas.getLastXMoves(2)).toEqual([
|
||||
expect.objectContaining({
|
||||
move: MoveId.SWORDS_DANCE,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.FOLLOW_UP,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
move: MoveId.SLEEP_TALK,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should fail if the user is not asleep", async () => {
|
||||
game.override.statusEffect(StatusEffect.POISON);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
@ -45,6 +69,19 @@ describe("Moves - Sleep Talk", () => {
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail the turn the user wakes up from Sleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
feebas.status!.sleepTurnsRemaining = 1;
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(feebas).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should fail if the user has no valid moves", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
@ -54,22 +91,15 @@ describe("Moves - Sleep Talk", () => {
|
||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should call a random valid move if the user is asleep", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called
|
||||
it("should apply secondary effects of the called move", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.SCALE_SHOT]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK));
|
||||
});
|
||||
|
||||
it("should apply secondary effects of a move", async () => {
|
||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.WOOD_HAMMER]); // Dig and Fly are invalid moves, Wood Hammer should always be called
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(MoveId.SLEEP_TALK);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.field.getPlayerPokemon().isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas.getStatStage(Stat.SPD)).toBe(1);
|
||||
expect(feebas.getStatStage(Stat.DEF)).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
@ -63,7 +63,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Both pokemon fainted
|
||||
scene.getPlayerParty().forEach(p => {
|
||||
p.hp = 0;
|
||||
p.trySetStatus(StatusEffect.FAINT);
|
||||
p.doSetStatus(StatusEffect.FAINT);
|
||||
void p.updateInfo();
|
||||
});
|
||||
|
||||
@ -83,7 +83,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -102,7 +102,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -121,7 +121,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -167,7 +167,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].level = 100;
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
party[1].level = 10;
|
||||
|
||||
@ -206,7 +206,7 @@ describe("Mystery Encounter Utils", () => {
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].level = 10;
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].doSetStatus(StatusEffect.FAINT);
|
||||
await party[0].updateInfo();
|
||||
party[1].level = 100;
|
||||
|
||||
|
82
test/system/rename-run.test.ts
Normal file
82
test/system/rename-run.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as account from "#app/account";
|
||||
import * as bypassLoginModule from "#app/global-vars/bypass-login";
|
||||
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
|
||||
import type { SessionSaveData } from "#app/system/game-data";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("System - Rename Run", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.battleStyle("single")
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
describe("renameSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(false);
|
||||
vi.spyOn(account, "updateUserInfo").mockImplementation(async () => [true, 1]);
|
||||
});
|
||||
|
||||
it("should return false if slotId < 0", async () => {
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false if getSession returns null", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue(null as unknown as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(-1, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if bypassLogin is true", async () => {
|
||||
vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(true);
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if api returns error", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("Unknown Error!");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true if api is succesfull", async () => {
|
||||
vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData);
|
||||
vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("");
|
||||
|
||||
const result = await game.scene.gameData.renameSession(0, "Named Run");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
expect(account.updateUserInfo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user