mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-21 06:49:35 +02:00
Compare commits
9 Commits
9f476bfa94
...
af377937bf
Author | SHA1 | Date | |
---|---|---|---|
|
af377937bf | ||
|
f42237d415 | ||
|
b44f0a4176 | ||
|
076ef81691 | ||
|
23271901cf | ||
|
1517e0512e | ||
|
a29161c2ed | ||
|
314f46a22b | ||
|
4885a6abc5 |
@ -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.
|
- 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.
|
[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.
|
- 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).
|
3. Your locales should use the following format:
|
||||||
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
|
- File names should be in `kebab-case`. Example: `trainer-names.json`
|
||||||
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
|
- 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).
|
[^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.
|
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.
|
||||||
|
@ -104,6 +104,7 @@ import {
|
|||||||
getLuckString,
|
getLuckString,
|
||||||
getLuckTextTint,
|
getLuckTextTint,
|
||||||
getPartyLuckValue,
|
getPartyLuckValue,
|
||||||
|
type ModifierType,
|
||||||
PokemonHeldItemModifierType,
|
PokemonHeldItemModifierType,
|
||||||
} from "#modifiers/modifier-type";
|
} from "#modifiers/modifier-type";
|
||||||
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||||
@ -1203,7 +1204,9 @@ export class BattleScene extends SceneBase {
|
|||||||
this.updateScoreText();
|
this.updateScoreText();
|
||||||
this.scoreText.setVisible(false);
|
this.scoreText.setVisible(false);
|
||||||
|
|
||||||
[this.luckLabelText, this.luckText].map(t => t.setVisible(false));
|
[this.luckLabelText, this.luckText].forEach(t => {
|
||||||
|
t.setVisible(false);
|
||||||
|
});
|
||||||
|
|
||||||
this.newArena(Overrides.STARTING_BIOME_OVERRIDE || BiomeId.TOWN);
|
this.newArena(Overrides.STARTING_BIOME_OVERRIDE || BiomeId.TOWN);
|
||||||
|
|
||||||
@ -1237,8 +1240,7 @@ export class BattleScene extends SceneBase {
|
|||||||
Object.values(mp)
|
Object.values(mp)
|
||||||
.flat()
|
.flat()
|
||||||
.map(mt => mt.modifierType)
|
.map(mt => mt.modifierType)
|
||||||
.filter(mt => "localize" in mt)
|
.filter((mt): mt is ModifierType & Localizable => "localize" in mt && typeof mt.localize === "function"),
|
||||||
.map(lpb => lpb as unknown as Localizable),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (const item of localizable) {
|
for (const item of localizable) {
|
||||||
@ -1513,8 +1515,8 @@ export class BattleScene extends SceneBase {
|
|||||||
return this.currentBattle;
|
return this.currentBattle;
|
||||||
}
|
}
|
||||||
|
|
||||||
newArena(biome: BiomeId, playerFaints?: number): Arena {
|
newArena(biome: BiomeId, playerFaints = 0): Arena {
|
||||||
this.arena = new Arena(biome, BiomeId[biome].toLowerCase(), playerFaints);
|
this.arena = new Arena(biome, playerFaints);
|
||||||
this.eventTarget.dispatchEvent(new NewArenaEvent());
|
this.eventTarget.dispatchEvent(new NewArenaEvent());
|
||||||
|
|
||||||
this.arenaBg.pipelineData = {
|
this.arenaBg.pipelineData = {
|
||||||
@ -2711,7 +2713,9 @@ export class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.party.map(p => p.updateInfo(instant));
|
this.party.forEach(p => {
|
||||||
|
p.updateInfo(instant);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const args = [this];
|
const args = [this];
|
||||||
if (modifier.shouldApply(...args)) {
|
if (modifier.shouldApply(...args)) {
|
||||||
|
@ -74,6 +74,7 @@ import {
|
|||||||
randSeedItem,
|
randSeedItem,
|
||||||
toDmgValue,
|
toDmgValue,
|
||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
|
import { toCamelCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class Ability implements Localizable {
|
export class Ability implements Localizable {
|
||||||
@ -109,13 +110,9 @@ export class Ability implements Localizable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localize(): void {
|
localize(): void {
|
||||||
const i18nKey = AbilityId[this.id]
|
const i18nKey = toCamelCase(AbilityId[this.id]);
|
||||||
.split("_")
|
|
||||||
.filter(f => f)
|
|
||||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
|
||||||
.join("") as string;
|
|
||||||
|
|
||||||
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : "";
|
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`)}${this.nameAppend}` : "";
|
||||||
this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : "";
|
this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1152,13 +1152,16 @@ export class GravityTag extends SerializableArenaTag {
|
|||||||
|
|
||||||
onAdd(_arena: Arena): void {
|
onAdd(_arena: Arena): void {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
|
globalScene.phaseManager.queueMessage(i18next.t("arenaTag:gravityOnAdd"));
|
||||||
|
|
||||||
|
// Remove all flying-related effects from all on-field Pokemon.
|
||||||
globalScene.getField(true).forEach(pokemon => {
|
globalScene.getField(true).forEach(pokemon => {
|
||||||
if (pokemon !== null) {
|
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
pokemon.removeTag(BattlerTagType.FLYING);
|
||||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
// TODO: This is an extremely poor way of handling move interruption
|
||||||
}
|
|
||||||
|
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1866,17 +1866,16 @@ interface PokemonPrevolutions {
|
|||||||
export const pokemonPrevolutions: PokemonPrevolutions = {};
|
export const pokemonPrevolutions: PokemonPrevolutions = {};
|
||||||
|
|
||||||
export function initPokemonPrevolutions(): void {
|
export function initPokemonPrevolutions(): void {
|
||||||
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string);
|
// TODO: Why do we have empty strings in our array?
|
||||||
const prevolutionKeys = Object.keys(pokemonEvolutions);
|
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ];
|
||||||
prevolutionKeys.forEach(pk => {
|
for (const [pk, evolutions] of Object.entries(pokemonEvolutions)) {
|
||||||
const evolutions = pokemonEvolutions[pk];
|
|
||||||
for (const ev of evolutions) {
|
for (const ev of evolutions) {
|
||||||
if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) {
|
if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId;
|
pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -736,6 +736,10 @@ export class FlinchedTag extends BattlerTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag to cancel the target's action when knocked out of a flying move by Smack Down or Gravity.
|
||||||
|
*/
|
||||||
|
// TODO: This is not a very good way to cancel a semi invulnerable turn
|
||||||
export class InterruptedTag extends BattlerTag {
|
export class InterruptedTag extends BattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.INTERRUPTED;
|
public override readonly tagType = BattlerTagType.INTERRUPTED;
|
||||||
constructor(sourceMove: MoveId) {
|
constructor(sourceMove: MoveId) {
|
||||||
@ -765,7 +769,7 @@ export class InterruptedTag extends BattlerTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition) Confusion} status condition
|
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition) | Confusion} status condition
|
||||||
*/
|
*/
|
||||||
export class ConfusedTag extends SerializableBattlerTag {
|
export class ConfusedTag extends SerializableBattlerTag {
|
||||||
public override readonly tagType = BattlerTagType.CONFUSED;
|
public override readonly tagType = BattlerTagType.CONFUSED;
|
||||||
@ -774,8 +778,9 @@ export class ConfusedTag extends SerializableBattlerTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canAdd(pokemon: Pokemon): boolean {
|
canAdd(pokemon: Pokemon): boolean {
|
||||||
const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.MISTY;
|
const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.MISTY;
|
||||||
if (blockedByTerrain) {
|
if (blockedByTerrain) {
|
||||||
|
// TODO: this should not trigger if the current move is an attacking move
|
||||||
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
|
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindS
|
|||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { applyChallenges } from "#utils/challenge-utils";
|
import { applyChallenges } from "#utils/challenge-utils";
|
||||||
|
|
||||||
@ -162,10 +162,16 @@ export abstract class Move implements Localizable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localize(): void {
|
localize(): void {
|
||||||
const i18nKey = MoveId[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string;
|
const i18nKey = toCamelCase(MoveId[this.id])
|
||||||
|
|
||||||
this.name = this.id ? `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}` : "";
|
if (this.id === MoveId.NONE) {
|
||||||
this.effect = this.id ? `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}` : "";
|
this.name = "";
|
||||||
|
this.effect = ""
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}`;
|
||||||
|
this.effect = `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1530,6 +1536,7 @@ export class CritOnlyAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Fix subclasses to actually extend from `getDamage`
|
||||||
export class FixedDamageAttr extends MoveAttr {
|
export class FixedDamageAttr extends MoveAttr {
|
||||||
private damage: number;
|
private damage: number;
|
||||||
|
|
||||||
@ -5366,13 +5373,11 @@ export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
if (!target.isGrounded(true) && target.isOfType(PokemonType.FLYING)) {
|
||||||
const multiplier = args[0] as NumberHolder;
|
const multiplier = args[0];
|
||||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
// When a flying type is hit, the first hit is always 1x multiplier.
|
||||||
if (target.isOfType(PokemonType.FLYING)) {
|
multiplier.value = 1;
|
||||||
multiplier.value = 1;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5611,13 +5616,13 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
|||||||
protected cancelOnFail: boolean;
|
protected cancelOnFail: boolean;
|
||||||
private failOnOverlap: boolean;
|
private failOnOverlap: boolean;
|
||||||
|
|
||||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) {
|
constructor(tagType: BattlerTagType, selfTarget = false, failOnOverlap = false, turnCountMin: number = 0, turnCountMax = turnCountMin, lastHitOnly = false) {
|
||||||
super(selfTarget, { lastHitOnly: lastHitOnly });
|
super(selfTarget, { lastHitOnly: lastHitOnly });
|
||||||
|
|
||||||
this.tagType = tagType;
|
this.tagType = tagType;
|
||||||
this.turnCountMin = turnCountMin;
|
this.turnCountMin = turnCountMin;
|
||||||
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
|
this.turnCountMax = turnCountMax;
|
||||||
this.failOnOverlap = !!failOnOverlap;
|
this.failOnOverlap = failOnOverlap;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
@ -5627,13 +5632,14 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
|||||||
|
|
||||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||||
if (moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) {
|
if (moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) {
|
||||||
return (this.selfTarget ? user : target).addTag(this.tagType, user.randBattleSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id);
|
return (this.selfTarget ? user : target).addTag(this.tagType, user.randBattleSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCondition(): MoveConditionFunc | null {
|
getCondition(): MoveConditionFunc | null {
|
||||||
|
// TODO: This should consider whether the tag can be added
|
||||||
return this.failOnOverlap
|
return this.failOnOverlap
|
||||||
? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType)
|
? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType)
|
||||||
: null;
|
: null;
|
||||||
@ -5711,8 +5717,10 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the appropriate battler tag for Smack Down and Thousand arrows
|
* Attribute to add the {@linkcode BattlerTagType.IGNORE_FLYING | IGNORE_FLYING} battler tag to the target
|
||||||
* @extends AddBattlerTagAttr
|
* and remove any prior sources of ungroundedness.
|
||||||
|
*
|
||||||
|
* Does nothing if the target was not already ungrounded.
|
||||||
*/
|
*/
|
||||||
export class FallDownAttr extends AddBattlerTagAttr {
|
export class FallDownAttr extends AddBattlerTagAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -5720,18 +5728,35 @@ export class FallDownAttr extends AddBattlerTagAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds Grounded Tag to the target and checks if fallDown message should be displayed
|
* Add `GroundedTag` to the target, remove all prior sources of ungroundedness
|
||||||
* @param user the {@linkcode Pokemon} using the move
|
* and display a message.
|
||||||
* @param target the {@linkcode Pokemon} targeted by the move
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
* @param move the {@linkcode Move} invoking this effect
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
|
* @param move - The {@linkcode Move} invoking this effect
|
||||||
* @param args n/a
|
* @param args n/a
|
||||||
* @returns `true` if the effect successfully applies; `false` otherwise
|
* @returns Whether the target was successfully brought down to earth.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
||||||
if (!target.isGrounded()) {
|
// Smack down and similar only add their tag if the target is already ungrounded,
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
// barring any prior semi-invulnerability.
|
||||||
|
if (target.isGrounded(true)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return super.apply(user, target, move, args);
|
|
||||||
|
// Remove the target's prior sources of ungroundedness.
|
||||||
|
// NB: These effects cannot simply be part of the tag's `onAdd` effect as Ingrain also adds the tag
|
||||||
|
// but does not remove Telekinesis' accuracy boost
|
||||||
|
target.removeTag(BattlerTagType.FLOATING);
|
||||||
|
target.removeTag(BattlerTagType.TELEKINESIS);
|
||||||
|
if (target.getTag(BattlerTagType.FLYING)) {
|
||||||
|
target.removeTag(BattlerTagType.FLYING);
|
||||||
|
// TODO: This is an extremely poor way of handling move interruption
|
||||||
|
target.addTag(BattlerTagType.INTERRUPTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fallDown", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||||
|
return super.apply(user, target, move, _args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5835,6 +5860,7 @@ export class CurseAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Delete this and make mortal spin use `RemoveBattlerTagAttr`
|
||||||
export class LapseBattlerTagAttr extends MoveEffectAttr {
|
export class LapseBattlerTagAttr extends MoveEffectAttr {
|
||||||
public tagTypes: BattlerTagType[];
|
public tagTypes: BattlerTagType[];
|
||||||
|
|
||||||
@ -8034,7 +8060,12 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
|
|||||||
return phase.isForcedLast() && slower;
|
return phase.isForcedLast() && slower;
|
||||||
};
|
};
|
||||||
|
|
||||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
// #region Condition functions
|
||||||
|
|
||||||
|
// TODO: This needs to become unselectable, not merely fail
|
||||||
|
const failOnGravityCondition: MoveConditionFunc = () => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||||
|
|
||||||
|
const failOnGroundedCondition: MoveConditionFunc = (_user, target) => !target.getTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
|
||||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||||
|
|
||||||
@ -8065,6 +8096,10 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke
|
|||||||
|
|
||||||
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
|
||||||
|
|
||||||
|
const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(PokemonType.UNKNOWN);
|
||||||
|
|
||||||
|
// #endregion Condition functions
|
||||||
|
|
||||||
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||||
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
const heldItems = target.getHeldItems().filter(i => i.isTransferable);
|
||||||
if (heldItems.length === 0) {
|
if (heldItems.length === 0) {
|
||||||
@ -8246,9 +8281,6 @@ export class ExposedMoveAttr extends AddBattlerTagAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(PokemonType.UNKNOWN);
|
|
||||||
|
|
||||||
export type MoveTargetSet = {
|
export type MoveTargetSet = {
|
||||||
targets: BattlerIndex[];
|
targets: BattlerIndex[];
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
@ -8895,7 +8927,6 @@ export function initMoves() {
|
|||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
/* Transform:
|
/* Transform:
|
||||||
* Does not copy the target's rage fist hit count
|
* Does not copy the target's rage fist hit count
|
||||||
* Does not copy the target's volatile status conditions (ie BattlerTags)
|
|
||||||
* Renders user typeless when copying typeless opponent (should revert to original typing)
|
* Renders user typeless when copying typeless opponent (should revert to original typing)
|
||||||
*/
|
*/
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
@ -9350,6 +9381,7 @@ export function initMoves() {
|
|||||||
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
||||||
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.INGRAIN, true, true)
|
||||||
|
// NB: We add IGNORE_FLYING and remove floating tag directly to avoid removing Telekinesis' accuracy boost
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, true)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true),
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLOATING ], true),
|
||||||
new AttackMove(MoveId.SUPERPOWER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
new AttackMove(MoveId.SUPERPOWER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
||||||
@ -9607,9 +9639,9 @@ export function initMoves() {
|
|||||||
.attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false)
|
||||||
.triageMove(),
|
.triageMove(),
|
||||||
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
new StatusMove(MoveId.GRAVITY, PokemonType.PSYCHIC, -1, 5, -1, 0, 4)
|
||||||
.ignoresProtect()
|
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
||||||
.target(MoveTarget.BOTH_SIDES),
|
.target(MoveTarget.BOTH_SIDES)
|
||||||
|
.ignoresProtect(),
|
||||||
new StatusMove(MoveId.MIRACLE_EYE, PokemonType.PSYCHIC, -1, 40, -1, 0, 4)
|
new StatusMove(MoveId.MIRACLE_EYE, PokemonType.PSYCHIC, -1, 40, -1, 0, 4)
|
||||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
@ -9739,7 +9771,8 @@ export function initMoves() {
|
|||||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||||
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
||||||
.condition((user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))),
|
.condition(failOnGravityCondition)
|
||||||
|
.condition(failOnGroundedCondition),
|
||||||
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
||||||
.attr(RecoilAttr, false, 0.33)
|
.attr(RecoilAttr, false, 0.33)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
@ -9968,12 +10001,12 @@ export function initMoves() {
|
|||||||
.powderMove()
|
.powderMove()
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true),
|
||||||
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
|
new StatusMove(MoveId.TELEKINESIS, PokemonType.PSYCHIC, -1, 15, -1, 0, 5)
|
||||||
.condition(failOnGravityCondition)
|
|
||||||
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
|
||||||
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
|
||||||
.condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
|
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
|
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
|
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
|
||||||
|
.condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId))
|
||||||
|
.condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega"))
|
||||||
|
.condition(failOnGravityCondition)
|
||||||
|
.condition(failOnGroundedCondition)
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
@ -9981,8 +10014,6 @@ export function initMoves() {
|
|||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
|
new AttackMove(MoveId.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
|
||||||
.attr(FallDownAttr)
|
.attr(FallDownAttr)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
new AttackMove(MoveId.STORM_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||||
@ -10444,8 +10475,6 @@ export function initMoves() {
|
|||||||
.attr(FallDownAttr)
|
.attr(FallDownAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6)
|
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6)
|
||||||
|
@ -12,6 +12,7 @@ import { WeatherType } from "#enums/weather-type";
|
|||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
|
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
|
||||||
import { type Constructor, coerceArray } from "#utils/common";
|
import { type Constructor, coerceArray } from "#utils/common";
|
||||||
|
import { toCamelCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export abstract class SpeciesFormChangeTrigger {
|
export abstract class SpeciesFormChangeTrigger {
|
||||||
@ -143,11 +144,7 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge
|
|||||||
super();
|
super();
|
||||||
this.move = move;
|
this.move = move;
|
||||||
this.known = known;
|
this.known = known;
|
||||||
const moveKey = MoveId[this.move]
|
const moveKey = toCamelCase(MoveId[this.move]);
|
||||||
.split("_")
|
|
||||||
.filter(f => f)
|
|
||||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
|
||||||
.join("") as unknown as string;
|
|
||||||
this.description = known
|
this.description = known
|
||||||
? i18next.t("pokemonEvolutions:Forms.moveLearned", {
|
? i18next.t("pokemonEvolutions:Forms.moveLearned", {
|
||||||
move: i18next.t(`move:${moveKey}.name`),
|
move: i18next.t(`move:${moveKey}.name`),
|
||||||
|
@ -23,17 +23,21 @@ export class Terrain {
|
|||||||
public terrainType: TerrainType;
|
public terrainType: TerrainType;
|
||||||
public turnsLeft: number;
|
public turnsLeft: number;
|
||||||
|
|
||||||
constructor(terrainType: TerrainType, turnsLeft?: number) {
|
constructor(terrainType: TerrainType, turnsLeft = 0) {
|
||||||
this.terrainType = terrainType;
|
this.terrainType = terrainType;
|
||||||
this.turnsLeft = turnsLeft || 0;
|
this.turnsLeft = turnsLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tick down this terrain's duration.
|
||||||
|
* @returns Whether the current terrain should remain active (`turnsLeft > 0`)
|
||||||
|
*/
|
||||||
lapse(): boolean {
|
lapse(): boolean {
|
||||||
if (this.turnsLeft) {
|
// TODO: Add separate flag for infinite duration terrains
|
||||||
return !!--this.turnsLeft;
|
if (this.turnsLeft <= 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return --this.turnsLeft > 0;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttackTypeMultiplier(attackType: PokemonType): number {
|
getAttackTypeMultiplier(attackType: PokemonType): number {
|
||||||
|
@ -20,20 +20,25 @@ export class Weather {
|
|||||||
public weatherType: WeatherType;
|
public weatherType: WeatherType;
|
||||||
public turnsLeft: number;
|
public turnsLeft: number;
|
||||||
|
|
||||||
constructor(weatherType: WeatherType, turnsLeft?: number) {
|
constructor(weatherType: WeatherType, turnsLeft = 0) {
|
||||||
this.weatherType = weatherType;
|
this.weatherType = weatherType;
|
||||||
this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0;
|
this.turnsLeft = this.isImmutable() ? 0 : turnsLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tick down this weather's duration.
|
||||||
|
* @returns Whether the current weather should remain active (`turnsLeft > 0`)
|
||||||
|
*/
|
||||||
lapse(): boolean {
|
lapse(): boolean {
|
||||||
if (this.isImmutable()) {
|
if (this.isImmutable()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.turnsLeft) {
|
|
||||||
return !!--this.turnsLeft;
|
if (this.turnsLeft <= 0) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return --this.turnsLeft > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
isImmutable(): boolean {
|
isImmutable(): boolean {
|
||||||
@ -127,6 +132,7 @@ export class Weather {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: These functions should return empty strings instead of `null` - requires bangs
|
||||||
export function getWeatherStartMessage(weatherType: WeatherType): string | null {
|
export function getWeatherStartMessage(weatherType: WeatherType): string | null {
|
||||||
switch (weatherType) {
|
switch (weatherType) {
|
||||||
case WeatherType.SUNNY:
|
case WeatherType.SUNNY:
|
||||||
|
@ -38,6 +38,7 @@ export enum UiMode {
|
|||||||
UNAVAILABLE,
|
UNAVAILABLE,
|
||||||
CHALLENGE_SELECT,
|
CHALLENGE_SELECT,
|
||||||
RENAME_POKEMON,
|
RENAME_POKEMON,
|
||||||
|
RENAME_RUN,
|
||||||
RUN_HISTORY,
|
RUN_HISTORY,
|
||||||
RUN_INFO,
|
RUN_INFO,
|
||||||
TEST_DIALOGUE,
|
TEST_DIALOGUE,
|
||||||
|
@ -54,7 +54,7 @@ export class Arena {
|
|||||||
public bgm: string;
|
public bgm: string;
|
||||||
public ignoreAbilities: boolean;
|
public ignoreAbilities: boolean;
|
||||||
public ignoringEffectSource: BattlerIndex | null;
|
public ignoringEffectSource: BattlerIndex | null;
|
||||||
public playerTerasUsed: number;
|
public playerTerasUsed = 0;
|
||||||
/**
|
/**
|
||||||
* Saves the number of times a party pokemon faints during a arena encounter.
|
* Saves the number of times a party pokemon faints during a arena encounter.
|
||||||
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
||||||
@ -68,12 +68,11 @@ export class Arena {
|
|||||||
|
|
||||||
public readonly eventTarget: EventTarget = new EventTarget();
|
public readonly eventTarget: EventTarget = new EventTarget();
|
||||||
|
|
||||||
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
constructor(biome: BiomeId, playerFaints = 0) {
|
||||||
this.biomeType = biome;
|
this.biomeType = biome;
|
||||||
this.bgm = bgm;
|
this.bgm = BiomeId[biome].toLowerCase();
|
||||||
this.trainerPool = biomeTrainerPools[biome];
|
this.trainerPool = biomeTrainerPools[biome];
|
||||||
this.updatePoolsForTimeOfDay();
|
this.updatePoolsForTimeOfDay();
|
||||||
this.playerTerasUsed = 0;
|
|
||||||
this.playerFaints = playerFaints;
|
this.playerFaints = playerFaints;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,20 +288,18 @@ export class Arena {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets weather to the override specified in overrides.ts
|
* Sets weather to the override specified in overrides.ts`
|
||||||
* @param weather new {@linkcode WeatherType} to set
|
|
||||||
* @returns true to force trySetWeather to return true
|
|
||||||
*/
|
*/
|
||||||
trySetWeatherOverride(weather: WeatherType): boolean {
|
private overrideWeather(): void {
|
||||||
|
const weather = Overrides.WEATHER_OVERRIDE;
|
||||||
this.weather = new Weather(weather, 0);
|
this.weather = new Weather(weather, 0);
|
||||||
globalScene.phaseManager.unshiftNew("CommonAnimPhase", undefined, undefined, CommonAnim.SUNNY + (weather - 1));
|
globalScene.phaseManager.unshiftNew("CommonAnimPhase", undefined, undefined, CommonAnim.SUNNY + (weather - 1));
|
||||||
globalScene.phaseManager.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct?
|
globalScene.phaseManager.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct?
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns weather or not the weather can be changed to {@linkcode weather} */
|
/** Returns weather or not the weather can be changed to {@linkcode weather} */
|
||||||
canSetWeather(weather: WeatherType): boolean {
|
canSetWeather(weather: WeatherType): boolean {
|
||||||
return !(this.weather?.weatherType === (weather || undefined));
|
return this.getWeatherType() !== weather;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -313,14 +310,15 @@ export class Arena {
|
|||||||
*/
|
*/
|
||||||
trySetWeather(weather: WeatherType, user?: Pokemon): boolean {
|
trySetWeather(weather: WeatherType, user?: Pokemon): boolean {
|
||||||
if (Overrides.WEATHER_OVERRIDE) {
|
if (Overrides.WEATHER_OVERRIDE) {
|
||||||
return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE);
|
this.overrideWeather();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canSetWeather(weather)) {
|
if (!this.canSetWeather(weather)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldWeatherType = this.weather?.weatherType || WeatherType.NONE;
|
const oldWeatherType = this.getWeatherType();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.weather?.isImmutable() &&
|
this.weather?.isImmutable() &&
|
||||||
@ -344,7 +342,7 @@ export class Arena {
|
|||||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
|
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.weather = weather ? new Weather(weather, weatherDuration.value) : null;
|
this.weather = weather === WeatherType.NONE ? null : new Weather(weather, weatherDuration.value);
|
||||||
this.eventTarget.dispatchEvent(
|
this.eventTarget.dispatchEvent(
|
||||||
new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!),
|
new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!),
|
||||||
); // TODO: is this bang correct?
|
); // TODO: is this bang correct?
|
||||||
@ -405,25 +403,24 @@ export class Arena {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns whether or not the terrain can be set to {@linkcode terrain} */
|
/** Return whether or not the terrain can be set to {@linkcode terrain} */
|
||||||
canSetTerrain(terrain: TerrainType): boolean {
|
canSetTerrain(terrain: TerrainType): boolean {
|
||||||
return !(this.terrain?.terrainType === (terrain || undefined));
|
return this.getTerrainType() !== terrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to set a new terrain effect to the battle
|
* Attempt to set the current terrain to the specified type.
|
||||||
* @param terrain {@linkcode TerrainType} new {@linkcode TerrainType} to set
|
* @param terrain - The {@linkcode TerrainType} to try and set.
|
||||||
* @param ignoreAnim boolean if the terrain animation should be ignored
|
* @param ignoreAnim - Whether to prevent showing an the animation; default `false`
|
||||||
* @param user {@linkcode Pokemon} that caused the terrain effect
|
* @param user - The {@linkcode Pokemon} creating the terrain (if any)
|
||||||
* @returns true if new terrain set, false if no terrain provided or attempting to set the same terrain as currently in use
|
* @returns Whether the terrain was successfully set.
|
||||||
*/
|
*/
|
||||||
trySetTerrain(terrain: TerrainType, ignoreAnim = false, user?: Pokemon): boolean {
|
trySetTerrain(terrain: TerrainType, ignoreAnim = false, user?: Pokemon): boolean {
|
||||||
if (!this.canSetTerrain(terrain)) {
|
if (!this.canSetTerrain(terrain)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldTerrainType = this.terrain?.terrainType || TerrainType.NONE;
|
const oldTerrainType = this.getTerrainType();
|
||||||
|
|
||||||
const terrainDuration = new NumberHolder(0);
|
const terrainDuration = new NumberHolder(0);
|
||||||
|
|
||||||
if (!isNullOrUndefined(user)) {
|
if (!isNullOrUndefined(user)) {
|
||||||
@ -431,7 +428,7 @@ export class Arena {
|
|||||||
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
|
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null;
|
this.terrain = terrain === TerrainType.NONE ? null : new Terrain(terrain, terrainDuration.value);
|
||||||
|
|
||||||
this.eventTarget.dispatchEvent(
|
this.eventTarget.dispatchEvent(
|
||||||
new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!),
|
new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!),
|
||||||
@ -465,6 +462,24 @@ export class Arena {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Attempt to override the terrain to the value set inside {@linkcode Overrides.STARTING_TERRAIN_OVERRIDE}. */
|
||||||
|
tryOverrideTerrain(): void {
|
||||||
|
const terrain = Overrides.STARTING_TERRAIN_OVERRIDE;
|
||||||
|
if (terrain === TerrainType.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add a flag for permanent terrains
|
||||||
|
this.terrain = new Terrain(terrain, 0);
|
||||||
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"CommonAnimPhase",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CommonAnim.MISTY_TERRAIN + (terrain - 1),
|
||||||
|
);
|
||||||
|
globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain) ?? ""); // TODO: Remove `?? ""` when terrain-fail-msg branch removes `null` from these signatures
|
||||||
|
}
|
||||||
|
|
||||||
public isMoveWeatherCancelled(user: Pokemon, move: Move): boolean {
|
public isMoveWeatherCancelled(user: Pokemon, move: Move): boolean {
|
||||||
return !!this.weather && !this.weather.isEffectSuppressed() && this.weather.isMoveWeatherCancelled(user, move);
|
return !!this.weather && !this.weather.isEffectSuppressed() && this.weather.isMoveWeatherCancelled(user, move);
|
||||||
}
|
}
|
||||||
|
@ -2280,13 +2280,29 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return this.teraType;
|
return this.teraType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isGrounded(): boolean {
|
/**
|
||||||
|
* Return whether this Pokemon is currently on the ground.
|
||||||
|
*
|
||||||
|
* To be considered grounded, a Pokemon must either:
|
||||||
|
* * Be {@linkcode GroundedTag | forcibly grounded} from an effect like Smack Down or Ingrain
|
||||||
|
* * Be under the effects of {@linkcode ArenaTagType.GRAVITY | harsh gravity}
|
||||||
|
* * **Not** be all of the following things:
|
||||||
|
* * {@linkcode PokemonType.FLYING | Flying-type}
|
||||||
|
* * {@linkcode AbilityId.LEVITATE | Levitating}
|
||||||
|
* * {@linkcode BattlerTagType.FLOATING | Floating} from Magnet Rise or Telekinesis.
|
||||||
|
* * {@linkcode SemiInvulnerableTag | Semi-invulnerable} with `ignoreSemiInvulnerable` set to `false`
|
||||||
|
* @param ignoreSemiInvulnerable - Whether to ignore the target's semi-invulnerable state when determining groundedness;
|
||||||
|
default `false`
|
||||||
|
* @returns Whether this pokemon is currently grounded, as described above.
|
||||||
|
*/
|
||||||
|
public isGrounded(ignoreSemiInvulnerable = false): boolean {
|
||||||
return (
|
return (
|
||||||
!!this.getTag(GroundedTag) ||
|
!!this.getTag(GroundedTag) ||
|
||||||
|
globalScene.arena.hasTag(ArenaTagType.GRAVITY) ||
|
||||||
(!this.isOfType(PokemonType.FLYING, true, true) &&
|
(!this.isOfType(PokemonType.FLYING, true, true) &&
|
||||||
!this.hasAbility(AbilityId.LEVITATE) &&
|
!this.hasAbility(AbilityId.LEVITATE) &&
|
||||||
!this.getTag(BattlerTagType.FLOATING) &&
|
!this.getTag(BattlerTagType.FLOATING) &&
|
||||||
!this.getTag(SemiInvulnerableTag))
|
(ignoreSemiInvulnerable || !this.getTag(SemiInvulnerableTag)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2486,7 +2502,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
// Handle flying v ground type immunity without removing flying type so effective types are still effective
|
// Handle flying v ground type immunity without removing flying type so effective types are still effective
|
||||||
// Related to https://github.com/pagefaultgames/pokerogue/issues/524
|
// Related to https://github.com/pagefaultgames/pokerogue/issues/524
|
||||||
if (moveType === PokemonType.GROUND && (this.isGrounded() || arena.hasTag(ArenaTagType.GRAVITY))) {
|
if (moveType === PokemonType.GROUND && this.isGrounded()) {
|
||||||
const flyingIndex = types.indexOf(PokemonType.FLYING);
|
const flyingIndex = types.indexOf(PokemonType.FLYING);
|
||||||
if (flyingIndex > -1) {
|
if (flyingIndex > -1) {
|
||||||
types.splice(flyingIndex, 1);
|
types.splice(flyingIndex, 1);
|
||||||
@ -3753,6 +3769,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||||
|
|
||||||
/** Combined damage multiplier from field effects such as weather, terrain, etc. */
|
/** Combined damage multiplier from field effects such as weather, terrain, etc. */
|
||||||
|
// TODO: This should be applied directly to base power
|
||||||
const arenaAttackTypeMultiplier = new NumberHolder(
|
const arenaAttackTypeMultiplier = new NumberHolder(
|
||||||
globalScene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()),
|
globalScene.arena.getAttackTypeMultiplier(moveType, source.isGrounded()),
|
||||||
);
|
);
|
||||||
|
@ -447,7 +447,9 @@ export class LoadingScene extends SceneBase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!mobile) {
|
if (!mobile) {
|
||||||
loadingGraphics.map(g => g.setVisible(false));
|
loadingGraphics.forEach(g => {
|
||||||
|
g.setVisible(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const intro = this.add.video(0, 0);
|
const intro = this.add.video(0, 0);
|
||||||
|
@ -121,8 +121,8 @@ export class ModifierBar extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateModifierOverflowVisibility(ignoreLimit: boolean) {
|
updateModifierOverflowVisibility(ignoreLimit: boolean) {
|
||||||
const modifierIcons = this.getAll().reverse();
|
const modifierIcons = this.getAll().reverse() as Phaser.GameObjects.Container[];
|
||||||
for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) {
|
for (const modifier of modifierIcons.slice(iconOverflowIndex)) {
|
||||||
modifier.setVisible(ignoreLimit);
|
modifier.setVisible(ignoreLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { type PokeballCounts } from "#app/battle-scene";
|
import { type PokeballCounts } from "#app/battle-scene";
|
||||||
|
import { TerrainType } from "#app/data/terrain";
|
||||||
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
import { EvolutionItem } from "#balance/pokemon-evolutions";
|
||||||
import { Gender } from "#data/gender";
|
import { Gender } from "#data/gender";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -61,6 +62,12 @@ class DefaultOverrides {
|
|||||||
readonly SEED_OVERRIDE: string = "";
|
readonly SEED_OVERRIDE: string = "";
|
||||||
readonly DAILY_RUN_SEED_OVERRIDE: string | null = null;
|
readonly DAILY_RUN_SEED_OVERRIDE: string | null = null;
|
||||||
readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
|
readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
|
||||||
|
/**
|
||||||
|
* If set, will override the in-game terrain at the start of each biome transition.
|
||||||
|
*
|
||||||
|
* Lasts until cleared or replaced by another effect, and is refreshed at the start of each new biome.
|
||||||
|
*/
|
||||||
|
readonly STARTING_TERRAIN_OVERRIDE: TerrainType = TerrainType.NONE;
|
||||||
/**
|
/**
|
||||||
* If `null`, ignore this override.
|
* If `null`, ignore this override.
|
||||||
*
|
*
|
||||||
|
@ -690,6 +690,7 @@ export class EncounterPhase extends BattlePhase {
|
|||||||
trySetWeatherIfNewBiome(): void {
|
trySetWeatherIfNewBiome(): void {
|
||||||
if (!this.loaded) {
|
if (!this.loaded) {
|
||||||
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
|
globalScene.arena.trySetWeather(getRandomWeatherType(globalScene.arena));
|
||||||
|
globalScene.arena.tryOverrideTerrain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common";
|
|||||||
|
|
||||||
export class WeatherEffectPhase extends CommonAnimPhase {
|
export class WeatherEffectPhase extends CommonAnimPhase {
|
||||||
public readonly phaseName = "WeatherEffectPhase";
|
public readonly phaseName = "WeatherEffectPhase";
|
||||||
public weather: Weather | null;
|
public weather: Weather | null; // TODO: This should not be `null`
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
|
@ -127,6 +127,7 @@ export interface SessionSaveData {
|
|||||||
battleType: BattleType;
|
battleType: BattleType;
|
||||||
trainer: TrainerData;
|
trainer: TrainerData;
|
||||||
gameVersion: string;
|
gameVersion: string;
|
||||||
|
runNameText: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
challenges: ChallengeData[];
|
challenges: ChallengeData[];
|
||||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||||
@ -206,10 +207,12 @@ export interface StarterData {
|
|||||||
[key: number]: StarterDataEntry;
|
[key: number]: StarterDataEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TutorialFlags {
|
// TODO: Rework into a bitmask
|
||||||
[key: string]: boolean;
|
export type TutorialFlags = {
|
||||||
}
|
[key in Tutorial]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Rework into a bitmask
|
||||||
export interface SeenDialogues {
|
export interface SeenDialogues {
|
||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
}
|
}
|
||||||
@ -822,52 +825,51 @@ export class GameData {
|
|||||||
return true; // TODO: is `true` the correct return value?
|
return true; // TODO: is `true` the correct return value?
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadGamepadSettings(): boolean {
|
private loadGamepadSettings(): void {
|
||||||
Object.values(SettingGamepad)
|
Object.values(SettingGamepad).forEach(setting => {
|
||||||
.map(setting => setting as SettingGamepad)
|
setSettingGamepad(setting, settingGamepadDefaults[setting]);
|
||||||
.forEach(setting => setSettingGamepad(setting, settingGamepadDefaults[setting]));
|
});
|
||||||
|
|
||||||
if (!localStorage.hasOwnProperty("settingsGamepad")) {
|
if (!localStorage.hasOwnProperty("settingsGamepad")) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct?
|
const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct?
|
||||||
|
|
||||||
for (const setting of Object.keys(settingsGamepad)) {
|
for (const setting of Object.keys(settingsGamepad)) {
|
||||||
setSettingGamepad(setting as SettingGamepad, settingsGamepad[setting]);
|
setSettingGamepad(setting as SettingGamepad, settingsGamepad[setting]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; // TODO: is `true` the correct return value?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean {
|
/**
|
||||||
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
* Save the specified tutorial as having the specified completion status.
|
||||||
let tutorials: object = {};
|
* @param tutorial - The {@linkcode Tutorial} whose completion status is being saved
|
||||||
if (localStorage.hasOwnProperty(key)) {
|
* @param status - The completion status to set
|
||||||
tutorials = JSON.parse(localStorage.getItem(key)!); // TODO: is this bang correct?
|
*/
|
||||||
|
public saveTutorialFlag(tutorial: Tutorial, status: boolean): void {
|
||||||
|
// Grab the prior save data tutorial
|
||||||
|
const saveDataKey = getDataTypeKey(GameDataType.TUTORIALS);
|
||||||
|
const tutorials: TutorialFlags = localStorage.hasOwnProperty(saveDataKey)
|
||||||
|
? JSON.parse(localStorage.getItem(saveDataKey)!)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// TODO: We shouldn't be storing this like that
|
||||||
|
for (const key of Object.values(Tutorial)) {
|
||||||
|
if (key === tutorial) {
|
||||||
|
tutorials[key] = status;
|
||||||
|
} else {
|
||||||
|
tutorials[key] ??= false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(Tutorial)
|
localStorage.setItem(saveDataKey, JSON.stringify(tutorials));
|
||||||
.map(t => t as Tutorial)
|
|
||||||
.forEach(t => {
|
|
||||||
const key = Tutorial[t];
|
|
||||||
if (key === tutorial) {
|
|
||||||
tutorials[key] = flag;
|
|
||||||
} else {
|
|
||||||
tutorials[key] ??= false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(key, JSON.stringify(tutorials));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTutorialFlags(): TutorialFlags {
|
public getTutorialFlags(): TutorialFlags {
|
||||||
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
const key = getDataTypeKey(GameDataType.TUTORIALS);
|
||||||
const ret: TutorialFlags = {};
|
const ret: TutorialFlags = Object.values(Tutorial).reduce((acc, tutorial) => {
|
||||||
Object.values(Tutorial)
|
acc[Tutorial[tutorial]] = false;
|
||||||
.map(tutorial => tutorial as Tutorial)
|
return acc;
|
||||||
.forEach(tutorial => (ret[Tutorial[tutorial]] = false));
|
}, {} as TutorialFlags);
|
||||||
|
|
||||||
if (!localStorage.hasOwnProperty(key)) {
|
if (!localStorage.hasOwnProperty(key)) {
|
||||||
return ret;
|
return ret;
|
||||||
@ -979,6 +981,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> {
|
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
|
||||||
return new Promise(async (resolve, reject) => {
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
|
|||||||
import { UiHandler } from "#ui/ui-handler";
|
import { UiHandler } from "#ui/ui-handler";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common";
|
import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common";
|
||||||
|
import { toCamelCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle";
|
import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle";
|
||||||
|
|
||||||
@ -207,6 +208,10 @@ export class RunInfoUiHandler extends UiHandler {
|
|||||||
headerText.setOrigin(0, 0);
|
headerText.setOrigin(0, 0);
|
||||||
headerText.setPositionRelative(headerBg, 8, 4);
|
headerText.setPositionRelative(headerBg, 8, 4);
|
||||||
this.runContainer.add(headerText);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -702,10 +707,7 @@ export class RunInfoUiHandler extends UiHandler {
|
|||||||
rules.push(i18next.t("challenges:inverseBattle.shortName"));
|
rules.push(i18next.t("challenges:inverseBattle.shortName"));
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
const localizationKey = Challenges[this.runInfo.challenges[i].id]
|
const localizationKey = toCamelCase(Challenges[this.runInfo.challenges[i].id]);
|
||||||
.split("_")
|
|
||||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
|
||||||
.join("");
|
|
||||||
rules.push(i18next.t(`challenges:${localizationKey}.name`));
|
rules.push(i18next.t(`challenges:${localizationKey}.name`));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { GameMode } from "#app/game-mode";
|
import { GameMode } from "#app/game-mode";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
|
import { GameModes } from "#enums/game-modes";
|
||||||
import { TextStyle } from "#enums/text-style";
|
import { TextStyle } from "#enums/text-style";
|
||||||
import { UiMode } from "#enums/ui-mode";
|
import { UiMode } from "#enums/ui-mode";
|
||||||
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
|
||||||
import * as Modifier from "#modifiers/modifier";
|
import * as Modifier from "#modifiers/modifier";
|
||||||
import type { SessionSaveData } from "#system/game-data";
|
import type { SessionSaveData } from "#system/game-data";
|
||||||
import type { PokemonData } from "#system/pokemon-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 { MessageUiHandler } from "#ui/message-ui-handler";
|
||||||
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
import { RunDisplayMode } from "#ui/run-info-ui-handler";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
const SESSION_SLOTS_COUNT = 5;
|
const SESSION_SLOTS_COUNT = 5;
|
||||||
const SLOTS_ON_SCREEN = 3;
|
const SLOTS_ON_SCREEN = 2;
|
||||||
|
|
||||||
export enum SaveSlotUiMode {
|
export enum SaveSlotUiMode {
|
||||||
LOAD,
|
LOAD,
|
||||||
@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
private uiMode: SaveSlotUiMode;
|
private uiMode: SaveSlotUiMode;
|
||||||
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
private saveSlotSelectCallback: SaveSlotSelectCallback | null;
|
||||||
|
protected manageDataConfig: OptionSelectConfig;
|
||||||
|
|
||||||
private scrollCursor = 0;
|
private scrollCursor = 0;
|
||||||
|
|
||||||
@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
processInput(button: Button): boolean {
|
processInput(button: Button): boolean {
|
||||||
const ui = this.getUi();
|
const ui = this.getUi();
|
||||||
|
const manageDataOptions: any[] = [];
|
||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
let error = false;
|
let error = false;
|
||||||
@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
const originalCallback = this.saveSlotSelectCallback;
|
const originalCallback = this.saveSlotSelectCallback;
|
||||||
if (button === Button.ACTION) {
|
if (button === Button.ACTION) {
|
||||||
const cursor = this.cursor + this.scrollCursor;
|
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;
|
error = true;
|
||||||
} else {
|
} else {
|
||||||
switch (this.uiMode) {
|
switch (this.uiMode) {
|
||||||
case SaveSlotUiMode.LOAD:
|
case SaveSlotUiMode.LOAD:
|
||||||
this.saveSlotSelectCallback = null;
|
if (!sessionSlot.malformed) {
|
||||||
originalCallback?.(cursor);
|
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;
|
break;
|
||||||
|
|
||||||
case SaveSlotUiMode.SAVE: {
|
case SaveSlotUiMode.SAVE: {
|
||||||
const saveAndCallback = () => {
|
const saveAndCallback = () => {
|
||||||
const originalCallback = this.saveSlotSelectCallback;
|
const originalCallback = this.saveSlotSelectCallback;
|
||||||
@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.saveSlotSelectCallback = null;
|
this.saveSlotSelectCallback = null;
|
||||||
|
ui.showText("", 0);
|
||||||
originalCallback?.(-1);
|
originalCallback?.(-1);
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
this.cursorObj = globalScene.add.container(0, 0);
|
this.cursorObj = globalScene.add.container(0, 0);
|
||||||
const cursorBox = globalScene.add.nineslice(
|
const cursorBox = globalScene.add.nineslice(
|
||||||
0,
|
0,
|
||||||
0,
|
15,
|
||||||
"select_cursor_highlight_thick",
|
"select_cursor_highlight_thick",
|
||||||
undefined,
|
undefined,
|
||||||
296,
|
294,
|
||||||
44,
|
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
);
|
);
|
||||||
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
const rightArrow = globalScene.add.image(0, 0, "cursor");
|
||||||
rightArrow.setPosition(160, 0);
|
rightArrow.setPosition(160, 15);
|
||||||
rightArrow.setName("rightArrow");
|
rightArrow.setName("rightArrow");
|
||||||
this.cursorObj.add([cursorBox, rightArrow]);
|
this.cursorObj.add([cursorBox, rightArrow]);
|
||||||
this.sessionSlotsContainer.add(this.cursorObj);
|
this.sessionSlotsContainer.add(this.cursorObj);
|
||||||
}
|
}
|
||||||
const cursorPosition = cursor + this.scrollCursor;
|
const cursorPosition = cursor + this.scrollCursor;
|
||||||
const cursorIncrement = cursorPosition * 56;
|
const cursorIncrement = cursorPosition * 76;
|
||||||
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
|
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.
|
// 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.
|
// Only session slots with session data will move leftwards and have a visible arrow.
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
this.cursorObj.setPosition(151, 26 + cursorIncrement);
|
this.cursorObj.setPosition(151, 20 + cursorIncrement);
|
||||||
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
|
||||||
} else {
|
} else {
|
||||||
this.cursorObj.setPosition(145, 26 + cursorIncrement);
|
this.cursorObj.setPosition(145, 20 + cursorIncrement);
|
||||||
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
|
||||||
}
|
}
|
||||||
this.setArrowVisibility(hasData);
|
this.setArrowVisibility(hasData);
|
||||||
@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
revertSessionSlot(slotIndex: number): void {
|
revertSessionSlot(slotIndex: number): void {
|
||||||
const sessionSlot = this.sessionSlots[slotIndex];
|
const sessionSlot = this.sessionSlots[slotIndex];
|
||||||
if (sessionSlot) {
|
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);
|
this.setCursor(this.cursor, prevSlotIndex);
|
||||||
globalScene.tweens.add({
|
globalScene.tweens.add({
|
||||||
targets: this.sessionSlotsContainer,
|
targets: this.sessionSlotsContainer,
|
||||||
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
|
y: this.sessionSlotsContainerInitialY - 76 * scrollCursor,
|
||||||
duration: fixedInt(325),
|
duration: fixedInt(325),
|
||||||
ease: "Sine.easeInOut",
|
ease: "Sine.easeInOut",
|
||||||
});
|
});
|
||||||
@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
class SessionSlot extends Phaser.GameObjects.Container {
|
class SessionSlot extends Phaser.GameObjects.Container {
|
||||||
public slotId: number;
|
public slotId: number;
|
||||||
public hasData: boolean;
|
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;
|
private loadingLabel: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
public saveData: SessionSaveData;
|
public saveData: SessionSaveData;
|
||||||
|
|
||||||
constructor(slotId: number) {
|
constructor(slotId: number) {
|
||||||
super(globalScene, 0, slotId * 56);
|
super(globalScene, 0, slotId * 76);
|
||||||
|
|
||||||
this.slotId = slotId;
|
this.slotId = slotId;
|
||||||
|
|
||||||
@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const slotWindow = addWindow(0, 0, 304, 52);
|
this.slotWindow = addWindow(0, 0, 304, 70);
|
||||||
this.add(slotWindow);
|
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.loadingLabel.setOrigin(0.5, 0.5);
|
||||||
this.add(this.loadingLabel);
|
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) {
|
async setupWithData(data: SessionSaveData) {
|
||||||
|
const hasName = data?.runNameText;
|
||||||
this.remove(this.loadingLabel, true);
|
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(
|
const gameModeLabel = addTextObject(
|
||||||
8,
|
8,
|
||||||
5,
|
19,
|
||||||
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
`${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`,
|
||||||
TextStyle.WINDOW,
|
TextStyle.WINDOW,
|
||||||
);
|
);
|
||||||
this.add(gameModeLabel);
|
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);
|
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);
|
this.add(playTimeLabel);
|
||||||
|
|
||||||
const pokemonIconsContainer = globalScene.add.container(144, 4);
|
const pokemonIconsContainer = globalScene.add.container(144, 16);
|
||||||
data.party.forEach((p: PokemonData, i: number) => {
|
data.party.forEach((p: PokemonData, i: number) => {
|
||||||
const iconContainer = globalScene.add.container(26 * i, 0);
|
const iconContainer = globalScene.add.container(26 * i, 0);
|
||||||
iconContainer.setScale(0.75);
|
iconContainer.setScale(0.75);
|
||||||
@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
TextStyle.PARTY,
|
TextStyle.PARTY,
|
||||||
{ fontSize: "54px", color: "#f8f8f8" },
|
{ fontSize: "54px", color: "#f8f8f8" },
|
||||||
);
|
);
|
||||||
text.setShadow(0, 0, undefined);
|
text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0);
|
||||||
text.setStroke("#424242", 14);
|
|
||||||
text.setOrigin(1, 0);
|
|
||||||
|
|
||||||
iconContainer.add(icon);
|
|
||||||
iconContainer.add(text);
|
|
||||||
|
|
||||||
|
iconContainer.add([icon, text]);
|
||||||
pokemonIconsContainer.add(iconContainer);
|
pokemonIconsContainer.add(iconContainer);
|
||||||
|
|
||||||
pokemon.destroy();
|
pokemon.destroy();
|
||||||
@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
this.add(pokemonIconsContainer);
|
this.add(pokemonIconsContainer);
|
||||||
|
|
||||||
const modifierIconsContainer = globalScene.add.container(148, 30);
|
const modifierIconsContainer = globalScene.add.container(148, 38);
|
||||||
modifierIconsContainer.setScale(0.5);
|
modifierIconsContainer.setScale(0.5);
|
||||||
let visibleModifierIndex = 0;
|
let visibleModifierIndex = 0;
|
||||||
for (const m of data.modifiers) {
|
for (const m of data.modifiers) {
|
||||||
@ -464,22 +627,33 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
load(): Promise<boolean> {
|
load(): Promise<boolean> {
|
||||||
return new Promise<boolean>(resolve => {
|
return new Promise<boolean>(resolve => {
|
||||||
globalScene.gameData.getSession(this.slotId).then(async sessionData => {
|
globalScene.gameData
|
||||||
// Ignore the results if the view was exited
|
.getSession(this.slotId)
|
||||||
if (!this.active) {
|
.then(async sessionData => {
|
||||||
return;
|
// Ignore the results if the view was exited
|
||||||
}
|
if (!this.active) {
|
||||||
if (!sessionData) {
|
return;
|
||||||
this.hasData = false;
|
}
|
||||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
this.hasData = !!sessionData;
|
||||||
resolve(false);
|
if (!sessionData) {
|
||||||
return;
|
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||||
}
|
resolve(false);
|
||||||
this.hasData = true;
|
return;
|
||||||
this.saveData = sessionData;
|
}
|
||||||
await this.setupWithData(sessionData);
|
this.saveData = sessionData;
|
||||||
resolve(true);
|
this.setupWithData(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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler {
|
|||||||
// we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
|
// we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key
|
||||||
|
|
||||||
// Return in the format expected by i18next
|
// Return in the format expected by i18next
|
||||||
return middleKey ? `${topKey}:${middleKey.map(m => m).join(".")}.${t}` : `${topKey}:${t}`;
|
return middleKey ? `${topKey}:${middleKey.join(".")}.${t}` : `${topKey}:${t}`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(t => t);
|
.filter(t => t);
|
||||||
|
@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme";
|
|||||||
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler";
|
||||||
import { executeIf } from "#utils/common";
|
import { executeIf } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import { RenameRunFormUiHandler } from "./rename-run-ui-handler";
|
||||||
|
|
||||||
const transitionModes = [
|
const transitionModes = [
|
||||||
UiMode.SAVE_SLOT,
|
UiMode.SAVE_SLOT,
|
||||||
@ -98,6 +99,7 @@ const noTransitionModes = [
|
|||||||
UiMode.SESSION_RELOAD,
|
UiMode.SESSION_RELOAD,
|
||||||
UiMode.UNAVAILABLE,
|
UiMode.UNAVAILABLE,
|
||||||
UiMode.RENAME_POKEMON,
|
UiMode.RENAME_POKEMON,
|
||||||
|
UiMode.RENAME_RUN,
|
||||||
UiMode.TEST_DIALOGUE,
|
UiMode.TEST_DIALOGUE,
|
||||||
UiMode.AUTO_COMPLETE,
|
UiMode.AUTO_COMPLETE,
|
||||||
UiMode.ADMIN,
|
UiMode.ADMIN,
|
||||||
@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container {
|
|||||||
new UnavailableModalUiHandler(),
|
new UnavailableModalUiHandler(),
|
||||||
new GameChallengesUiHandler(),
|
new GameChallengesUiHandler(),
|
||||||
new RenameFormUiHandler(),
|
new RenameFormUiHandler(),
|
||||||
|
new RenameRunFormUiHandler(),
|
||||||
new RunHistoryUiHandler(),
|
new RunHistoryUiHandler(),
|
||||||
new RunInfoUiHandler(),
|
new RunInfoUiHandler(),
|
||||||
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
new TestDialogueUiHandler(UiMode.TEST_DIALOGUE),
|
||||||
|
@ -278,27 +278,6 @@ describe("Abilities - Parental Bond", () => {
|
|||||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
|
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Smack Down boosted by this ability should only ground the target after the second hit", async () => {
|
|
||||||
game.override.moveset([MoveId.SMACK_DOWN]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const leadPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SMACK_DOWN);
|
|
||||||
await game.move.forceHit();
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("DamageAnimPhase");
|
|
||||||
|
|
||||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("U-turn boosted by this ability should strike twice before forcing a switch", async () => {
|
it("U-turn boosted by this ability should strike twice before forcing a switch", async () => {
|
||||||
game.override.moveset([MoveId.U_TURN]);
|
game.override.moveset([MoveId.U_TURN]);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Status } from "#data/status-effect";
|
import { Status } from "#data/status-effect";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
@ -104,27 +105,26 @@ describe("Abilities - SHIELDS DOWN", () => {
|
|||||||
expect(game.field.getPlayerPokemon().status).toBe(undefined);
|
expect(game.field.getPlayerPokemon().status).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
// toxic spikes currently does not poison flying types when gravity is in effect
|
it("should be poisoned by toxic spikes when Gravity is active before changing forms", async () => {
|
||||||
test.todo("should become poisoned by toxic spikes when grounded", async () => {
|
|
||||||
game.override
|
|
||||||
.enemyMoveset([MoveId.GRAVITY, MoveId.TOXIC_SPIKES, MoveId.SPLASH])
|
|
||||||
.moveset([MoveId.GRAVITY, MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]);
|
||||||
|
|
||||||
// turn 1
|
// Change minior to core form in a state where it would revert on switch
|
||||||
game.move.select(MoveId.GRAVITY);
|
const minior = game.scene.getPlayerParty()[1];
|
||||||
await game.move.selectEnemyMove(MoveId.TOXIC_SPIKES);
|
minior.formIndex = redCoreForm;
|
||||||
|
|
||||||
|
game.move.use(MoveId.GRAVITY);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TOXIC_SPIKES);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
// turn 2
|
expect(game).toHaveArenaTag(ArenaTagType.GRAVITY);
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
game.doSwitchPokemon(1);
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.MINIOR);
|
expect(minior.species.speciesId).toBe(SpeciesId.MINIOR);
|
||||||
expect(game.field.getPlayerPokemon().species.formIndex).toBe(0);
|
expect(minior.formIndex).toBe(0);
|
||||||
expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.POISON);
|
expect(minior.isGrounded()).toBe(true);
|
||||||
|
expect(minior).toHaveStatusEffect(StatusEffect.POISON);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should ignore yawn", async () => {
|
test("should ignore yawn", async () => {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
@ -27,130 +25,49 @@ describe("Arena - Gravity", () => {
|
|||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.moveset([MoveId.TACKLE, MoveId.GRAVITY, MoveId.FISSURE])
|
|
||||||
.ability(AbilityId.UNNERVE)
|
.ability(AbilityId.UNNERVE)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.enemySpecies(SpeciesId.SHUCKLE)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
|
||||||
.enemyLevel(5);
|
.enemyLevel(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
||||||
|
|
||||||
it("non-OHKO move accuracy is multiplied by 1.67", async () => {
|
it("should multiply all non-OHKO move accuracy by 1.67x", async () => {
|
||||||
const moveToCheck = allMoves[MoveId.TACKLE];
|
const accSpy = vi.spyOn(allMoves[MoveId.TACKLE], "calculateBattleAccuracy");
|
||||||
|
|
||||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
|
||||||
|
|
||||||
// Setup Gravity on first turn
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||||
game.move.select(MoveId.GRAVITY);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
game.move.use(MoveId.GRAVITY);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TACKLE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||||
|
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.TACKLE].accuracy * 1.67);
|
||||||
// Use non-OHKO move on second turn
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.TACKLE);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("OHKO move accuracy is not affected", async () => {
|
it("should not affect OHKO move accuracy", async () => {
|
||||||
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
|
const accSpy = vi.spyOn(allMoves[MoveId.FISSURE], "calculateBattleAccuracy");
|
||||||
const moveToCheck = allMoves[MoveId.FISSURE];
|
|
||||||
|
|
||||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
|
||||||
|
|
||||||
// Setup Gravity on first turn
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||||
game.move.select(MoveId.GRAVITY);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
game.move.use(MoveId.GRAVITY);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FISSURE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||||
|
expect(accSpy).toHaveLastReturnedWith(allMoves[MoveId.FISSURE].accuracy);
|
||||||
// Use OHKO move on second turn
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.FISSURE);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Against flying types", () => {
|
it("should forcibly ground all Pokemon for the duration of the effect", async () => {
|
||||||
it("can be hit by ground-type moves now", async () => {
|
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.EARTHQUAKE]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
const pidgeot = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
|
||||||
|
|
||||||
// Try earthquake on 1st turn (fails!);
|
|
||||||
game.move.select(MoveId.EARTHQUAKE);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
|
|
||||||
|
|
||||||
// Setup Gravity on 2nd turn
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.GRAVITY);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
|
||||||
|
|
||||||
// Use ground move on 3rd turn
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.EARTHQUAKE);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps super-effective moves super-effective after using gravity", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.PIDGEOT).moveset([MoveId.GRAVITY, MoveId.THUNDERBOLT]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
|
||||||
|
|
||||||
const pidgeot = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
|
||||||
|
|
||||||
// Setup Gravity on 1st turn
|
|
||||||
game.move.select(MoveId.GRAVITY);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
|
||||||
|
|
||||||
// Use electric move on 2nd turn
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.THUNDERBOLT);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cancels Fly if its user is semi-invulnerable", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.SNORLAX).enemyMoveset(MoveId.FLY).moveset([MoveId.GRAVITY, MoveId.SPLASH]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
|
||||||
|
|
||||||
const charizard = game.field.getPlayerPokemon();
|
|
||||||
const snorlax = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.GRAVITY);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
|
|
||||||
|
|
||||||
game.move.select(MoveId.GRAVITY);
|
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
expect(player.isGrounded()).toBe(true);
|
||||||
|
expect(enemy.isGrounded()).toBe(true);
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("Arena - Grassy Terrain", () => {
|
|
||||||
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(1)
|
|
||||||
.enemySpecies(SpeciesId.SHUCKLE)
|
|
||||||
.enemyAbility(AbilityId.STURDY)
|
|
||||||
.enemyMoveset(MoveId.FLY)
|
|
||||||
.moveset([MoveId.GRASSY_TERRAIN, MoveId.EARTHQUAKE])
|
|
||||||
.ability(AbilityId.NO_GUARD);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("halves the damage of Earthquake", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
|
||||||
|
|
||||||
const eq = allMoves[MoveId.EARTHQUAKE];
|
|
||||||
vi.spyOn(eq, "calculateBattlePower");
|
|
||||||
|
|
||||||
game.move.select(MoveId.EARTHQUAKE);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
expect(eq.calculateBattlePower).toHaveReturnedWith(100);
|
|
||||||
|
|
||||||
game.move.select(MoveId.GRASSY_TERRAIN);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.EARTHQUAKE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(eq.calculateBattlePower).toHaveReturnedWith(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Does not halve the damage of Earthquake if opponent is not grounded", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.NINJASK]);
|
|
||||||
|
|
||||||
const eq = allMoves[MoveId.EARTHQUAKE];
|
|
||||||
vi.spyOn(eq, "calculateBattlePower");
|
|
||||||
|
|
||||||
game.move.select(MoveId.GRASSY_TERRAIN);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.EARTHQUAKE);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(eq.calculateBattlePower).toHaveReturnedWith(100);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,59 +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 { WeatherType } from "#enums/weather-type";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("Arena - Psychic Terrain", () => {
|
|
||||||
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(1)
|
|
||||||
.enemySpecies(SpeciesId.SHUCKLE)
|
|
||||||
.enemyAbility(AbilityId.STURDY)
|
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
|
||||||
.moveset([MoveId.PSYCHIC_TERRAIN, MoveId.RAIN_DANCE, MoveId.DARK_VOID])
|
|
||||||
.ability(AbilityId.NO_GUARD);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Dark Void with Prankster is not blocked", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.PSYCHIC_TERRAIN);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.DARK_VOID);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
|
|
||||||
expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Rain Dance with Prankster is not blocked", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.PSYCHIC_TERRAIN);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.RAIN_DANCE);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
|
|
||||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN);
|
|
||||||
});
|
|
||||||
});
|
|
400
test/arena/terrain.test.ts
Normal file
400
test/arena/terrain.test.ts
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
import { allMoves } from "#app/data/data-lists";
|
||||||
|
import { getTerrainName, TerrainType } from "#app/data/terrain";
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { randSeedInt } from "#app/utils/common";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { WeatherType } from "#enums/weather-type";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import i18next from "i18next";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("Terrain -", () => {
|
||||||
|
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)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemySpecies(SpeciesId.SHUCKLE)
|
||||||
|
.enemyAbility(AbilityId.STURDY)
|
||||||
|
.passiveAbility(AbilityId.NO_GUARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Terrain boosts currently apply to damage dealt, not base power
|
||||||
|
describe.todo.each<{ name: string; type: PokemonType; terrain: TerrainType; move: MoveId }>([
|
||||||
|
{ name: "Electric", type: PokemonType.ELECTRIC, terrain: TerrainType.ELECTRIC, move: MoveId.THUNDERBOLT },
|
||||||
|
{ name: "Psychic", type: PokemonType.PSYCHIC, terrain: TerrainType.PSYCHIC, move: MoveId.PSYCHIC },
|
||||||
|
{ name: "Grassy", type: PokemonType.GRASS, terrain: TerrainType.GRASSY, move: MoveId.ENERGY_BALL },
|
||||||
|
{ name: "Misty", type: PokemonType.FAIRY, terrain: TerrainType.MISTY, move: MoveId.DRAGON_BREATH },
|
||||||
|
])("Common Tests - $name Terrain", ({ type, terrain, move }) => {
|
||||||
|
// biome-ignore lint/suspicious/noDuplicateTestHooks: This is a TODO test case
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override.terrain(terrain).enemyPassiveAbility(AbilityId.LEVITATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeStr = toTitleCase(PokemonType[type]);
|
||||||
|
|
||||||
|
it.skipIf(terrain === TerrainType.MISTY)(
|
||||||
|
`should boost power of grounded ${typeStr}-type moves by 1.3x, even against ungrounded targets`,
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||||
|
game.move.use(move);
|
||||||
|
await game.move.forceEnemyMove(move);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// Player grounded attack got boosted while enemy ungrounded attack didn't
|
||||||
|
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 1.3);
|
||||||
|
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(terrain === TerrainType.MISTY)(
|
||||||
|
"should cut power of grounded Dragon-type moves in half, even from ungrounded users",
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||||
|
game.move.use(move);
|
||||||
|
await game.move.forceEnemyMove(move);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// Enemy dragon breath got nerfed against grounded player; player dragon breath did not
|
||||||
|
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power);
|
||||||
|
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power * 0.5);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Move to a dedicated terrain pulse test file
|
||||||
|
it(`should change Terrain Pulse into a ${typeStr}-type move and double its base power`, async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
const powerSpy = vi.spyOn(allMoves[MoveId.TERRAIN_PULSE], "calculateBattlePower");
|
||||||
|
const playerTypeSpy = vi.spyOn(game.field.getPlayerPokemon(), "getMoveType");
|
||||||
|
const enemyTypeSpy = vi.spyOn(game.field.getEnemyPokemon(), "getMoveType");
|
||||||
|
|
||||||
|
game.move.use(MoveId.TERRAIN_PULSE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TERRAIN_PULSE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// player grounded terrain pulse was boosted & type converted; enemy ungrounded one wasn't
|
||||||
|
expect(powerSpy).toHaveLastReturnedWith(
|
||||||
|
allMoves[MoveId.TERRAIN_PULSE].power * (terrain === TerrainType.MISTY ? 2 : 2.6),
|
||||||
|
); // 2 * 1.3
|
||||||
|
expect(playerTypeSpy).toHaveLastReturnedWith(type);
|
||||||
|
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].power);
|
||||||
|
expect(enemyTypeSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Grassy Terrain", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override.terrain(TerrainType.GRASSY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should heal all grounded, non semi-invulnerable Pokemon for 1/16th max HP at end of turn", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
// blissey is grounded, shuckle isn't
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
blissey.hp /= 2;
|
||||||
|
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
game.field.mockAbility(shuckle, AbilityId.LEVITATE);
|
||||||
|
shuckle.hp /= 2;
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||||
|
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
||||||
|
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||||
|
|
||||||
|
game.move.use(MoveId.DIG);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// shuckle is airborne and blissey is semi-invulnerable, so nobody gets healed
|
||||||
|
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
||||||
|
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Enable once magnitude to return a specific power rating
|
||||||
|
it.todo.each<{ name: string; move: MoveId; basePower?: number }>([
|
||||||
|
{ name: "Bulldoze", move: MoveId.BULLDOZE },
|
||||||
|
{ name: "Earthquake", move: MoveId.EARTHQUAKE },
|
||||||
|
{ name: "Magnitude", move: MoveId.MAGNITUDE, basePower: 150 }, // magnitude 10
|
||||||
|
])(
|
||||||
|
"should halve $name's base power against grounded, on-field targets",
|
||||||
|
async ({ move, basePower = allMoves[move].power }) => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
||||||
|
// force high rolls for guaranteed magnitude 10s
|
||||||
|
vi.fn(randSeedInt).mockReturnValue(100);
|
||||||
|
|
||||||
|
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// Turn 1: attack with grassy terrain active; 0.5x
|
||||||
|
game.move.use(move);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(powerSpy).toHaveLastReturnedWith(basePower / 2);
|
||||||
|
|
||||||
|
// Turn 2: Give enemy levitate to make ungrounded and attack; 1x
|
||||||
|
// (hits due to no guard)
|
||||||
|
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.LEVITATE);
|
||||||
|
game.move.use(move);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||||
|
|
||||||
|
// Turn 3: Remove levitate and make enemy semi-invulnerable; 1x
|
||||||
|
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.BALL_FETCH);
|
||||||
|
game.move.use(move);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FLY);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemy.getLastXMoves()[0].move).toBe(MoveId.FLY);
|
||||||
|
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Electric Terrain", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override.terrain(TerrainType.ELECTRIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent all grounded Pokemon from being put to sleep", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPORE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const pidgeot = game.field.getPlayerPokemon();
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
expect(pidgeot.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
expect(shuckle.status?.effect).toBeUndefined();
|
||||||
|
// TODO: These don't work due to how move failures are propagated
|
||||||
|
// expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
// expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("terrain:defaultBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||||
|
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent attack moves from applying sleep without showing text/failing move", async () => {
|
||||||
|
vi.spyOn(allMoves[MoveId.RELIC_SONG], "chance", "get").mockReturnValue(100);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
const statusSpy = vi.spyOn(shuckle, "canSetStatus");
|
||||||
|
|
||||||
|
game.move.use(MoveId.RELIC_SONG);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
expect(shuckle.status?.effect).toBeUndefined();
|
||||||
|
expect(statusSpy).toHaveLastReturnedWith(false);
|
||||||
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
|
||||||
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
|
i18next.t("terrain:defaultBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||||
|
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Misty Terrain", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override.terrain(TerrainType.MISTY).enemyPassiveAbility(AbilityId.LEVITATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent all grounded Pokemon from gaining non-volatile statuses", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.TOXIC);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TOXIC);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// blissey is grounded & protected, shuckle isn't
|
||||||
|
expect(blissey.status?.effect).toBeUndefined();
|
||||||
|
expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||||
|
// TODO: These don't work due to how move failures are propagated
|
||||||
|
// expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
// expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("terrain:mistyBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block confusion and display message", async () => {
|
||||||
|
game.override.confusionActivation(false); // prevent self hits from cancelling move
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.CONFUSE_RAY);
|
||||||
|
await game.move.forceEnemyMove(MoveId.CONFUSE_RAY);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// blissey is grounded & protected, shuckle isn't
|
||||||
|
expect(blissey).not.toHaveBattlerTag(BattlerTagType.CONFUSED);
|
||||||
|
expect(shuckle).toHaveBattlerTag(BattlerTagType.CONFUSED);
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("terrain:mistyBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<{ status: string; move: MoveId }>([
|
||||||
|
{ status: "Sleep", move: MoveId.RELIC_SONG },
|
||||||
|
{ status: "Burn", move: MoveId.SACRED_FIRE },
|
||||||
|
{ status: "Freeze", move: MoveId.ICE_BEAM },
|
||||||
|
{ status: "Paralysis", move: MoveId.NUZZLE },
|
||||||
|
{ status: "Poison", move: MoveId.SLUDGE_BOMB },
|
||||||
|
{ status: "Toxic Poison", move: MoveId.MALIGNANT_CHAIN },
|
||||||
|
// TODO: Confusion currently displays terrain block message even from damaging moves
|
||||||
|
// { status: "Confusion", move: MoveId.MAGICAL_TORQUE },
|
||||||
|
])("should prevent attack moves from applying $status without showing text/failing move", async ({ move }) => {
|
||||||
|
vi.spyOn(allMoves[move], "chance", "get").mockReturnValue(100);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(move);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// Blissey was grounded and protected from effect, but still took damage
|
||||||
|
expect(blissey).not.toHaveFullHp();
|
||||||
|
expect(blissey).not.toHaveBattlerTag(BattlerTagType.CONFUSED);
|
||||||
|
expect(blissey.status?.effect).toBe(StatusEffect.NONE);
|
||||||
|
expect(shuckle).toHaveUsedMove({ result: MoveResult.SUCCESS });
|
||||||
|
|
||||||
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
|
i18next.t("terrain:mistyBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Psychic Terrain", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override.terrain(TerrainType.PSYCHIC).ability(AbilityId.GALE_WINGS).enemyAbility(AbilityId.PRANKSTER);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block all opponent-targeted priority moves", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.FAKE_OUT);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FOLLOW_ME);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("terrain:defaultBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||||
|
terrainName: getTerrainName(TerrainType.PSYCHIC),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should affect moves that only become priority due to abilities", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.FEATHER_DANCE);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
const shuckle = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("terrain:defaultBlockMessage", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
||||||
|
terrainName: getTerrainName(TerrainType.PSYCHIC),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<{ category: string; move: MoveId; effect: () => void }>([
|
||||||
|
{
|
||||||
|
category: "Field-targeted",
|
||||||
|
move: MoveId.RAIN_DANCE,
|
||||||
|
effect: () => {
|
||||||
|
expect(game.scene.arena.getWeatherType()).toBe(WeatherType.RAIN);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Enemy-targeting spread",
|
||||||
|
move: MoveId.DARK_VOID,
|
||||||
|
effect: () => {
|
||||||
|
expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])("should not block $category moves that become priority", async ({ move, effect }) => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const blissey = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
effect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
132
test/moves/fly-bounce.test.ts
Normal file
132
test/moves/fly-bounce.test.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
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 { 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("Moves - Fly and Bounce", () => {
|
||||||
|
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")
|
||||||
|
.ability(AbilityId.COMPOUND_EYES)
|
||||||
|
.enemySpecies(SpeciesId.SNORLAX)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.TACKLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Move to a global "charging moves" test file
|
||||||
|
it.each([
|
||||||
|
{ name: "Fly", move: MoveId.FLY },
|
||||||
|
{ name: "Bounce", move: MoveId.BOUNCE },
|
||||||
|
])("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.FLY);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
expect(player).toHaveBattlerTag(BattlerTagType.FLYING);
|
||||||
|
expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||||
|
expect(player.hp).toBe(player.getMaxHp());
|
||||||
|
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||||
|
expect(player.getMoveQueue()[0].move).toBe(MoveId.FLY);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(player).not.toHaveBattlerTag(BattlerTagType.FLYING);
|
||||||
|
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||||
|
expect(player.getMoveHistory()).toHaveLength(2);
|
||||||
|
|
||||||
|
const playerFly = player.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||||
|
expect(playerFly?.ppUsed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Move to a No Guard test file
|
||||||
|
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.NO_GUARD);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.FLY);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||||
|
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not expend PP when the attack phase is cancelled", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.NO_GUARD).enemyMoveset(MoveId.SPORE);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(MoveId.FLY);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(playerPokemon).not.toHaveBattlerTag(BattlerTagType.FLYING);
|
||||||
|
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
|
||||||
|
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
||||||
|
expect(playerFly?.ppUsed).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: We currently cancel Fly/Bounce in a really scuffed way
|
||||||
|
it.todo.each<{ name: string; move: MoveId }>([
|
||||||
|
{ name: "Smack Down", move: MoveId.SMACK_DOWN },
|
||||||
|
{ name: "Thousand Arrows", move: MoveId.THOUSAND_ARROWS },
|
||||||
|
{ name: "Gravity", move: MoveId.GRAVITY },
|
||||||
|
])("should be cancelled immediately when $name is used", async ({ move }) => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.AZURILL]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.BOUNCE);
|
||||||
|
await game.move.forceEnemyMove(move);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
|
||||||
|
// Bounce should've worked until hit
|
||||||
|
const azurill = game.field.getPlayerPokemon();
|
||||||
|
expect(azurill).toHaveBattlerTag(BattlerTagType.FLYING);
|
||||||
|
expect(azurill).not.toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
|
|
||||||
|
expect(azurill).not.toHaveBattlerTag(BattlerTagType.FLYING);
|
||||||
|
expect(azurill).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
expect(azurill.getMoveQueue()).toHaveLength(0);
|
||||||
|
expect(azurill.visible).toBe(true);
|
||||||
|
if (move !== MoveId.GRAVITY) {
|
||||||
|
expect(azurill.hp).toBeLessThan(azurill.getMaxHp());
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const snorlax = game.field.getEnemyPokemon();
|
||||||
|
expect(snorlax.hp).toBe(snorlax.getMaxHp());
|
||||||
|
});
|
||||||
|
});
|
@ -1,120 +0,0 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
|
||||||
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 { StatusEffect } from "#enums/status-effect";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
describe("Moves - Fly", () => {
|
|
||||||
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.FLY)
|
|
||||||
.battleStyle("single")
|
|
||||||
.startingLevel(100)
|
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
|
||||||
.enemyLevel(100)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.TACKLE);
|
|
||||||
|
|
||||||
vi.spyOn(allMoves[MoveId.FLY], "accuracy", "get").mockReturnValue(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FLY);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined();
|
|
||||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
|
||||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
|
||||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
|
||||||
expect(playerPokemon.getMoveQueue()[0].move).toBe(MoveId.FLY);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
|
||||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
|
||||||
|
|
||||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
|
||||||
expect(playerFly?.ppUsed).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
|
||||||
game.override.enemyAbility(AbilityId.NO_GUARD);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FLY);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
|
||||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not expend PP when the attack phase is cancelled", async () => {
|
|
||||||
game.override.enemyAbility(AbilityId.NO_GUARD).enemyMoveset(MoveId.SPORE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FLY);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
|
||||||
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
|
|
||||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
|
||||||
expect(playerFly?.ppUsed).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be cancelled when another Pokemon uses Gravity", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.SPLASH, MoveId.GRAVITY]);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FLY);
|
|
||||||
|
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
|
|
||||||
await game.toNextTurn();
|
|
||||||
await game.move.selectEnemyMove(MoveId.GRAVITY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
|
||||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
|
||||||
|
|
||||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === MoveId.FLY);
|
|
||||||
expect(playerFly?.ppUsed).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
@ -28,30 +29,16 @@ describe("Moves - Magnet Rise", () => {
|
|||||||
.enemyLevel(1);
|
.enemyLevel(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should make the user immune to ground-type moves", async () => {
|
it("should make the user ungrounded when used", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGNEZONE]);
|
await game.classicMode.startBattle([SpeciesId.MAGNEZONE]);
|
||||||
|
|
||||||
game.move.use(MoveId.MAGNET_RISE);
|
game.move.use(MoveId.MAGNET_RISE);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// magnezone levitated and was not hit by earthquake
|
||||||
const magnezone = game.field.getPlayerPokemon();
|
const magnezone = game.field.getPlayerPokemon();
|
||||||
expect(magnezone.hp).toBe(magnezone.getMaxHp());
|
expect(magnezone.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
||||||
expect(magnezone.isGrounded()).toBe(false);
|
expect(magnezone.isGrounded()).toBe(false);
|
||||||
});
|
|
||||||
|
|
||||||
it("should be removed by gravity", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGNEZONE]);
|
|
||||||
|
|
||||||
game.move.use(MoveId.MAGNET_RISE);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
const magnezone = game.field.getPlayerPokemon();
|
|
||||||
expect(magnezone.hp).toBe(magnezone.getMaxHp());
|
expect(magnezone.hp).toBe(magnezone.getMaxHp());
|
||||||
|
|
||||||
game.move.use(MoveId.GRAVITY);
|
|
||||||
await game.toEndOfTurn();
|
|
||||||
|
|
||||||
expect(magnezone.hp).toBeLessThan(magnezone.getMaxHp());
|
|
||||||
expect(magnezone.isGrounded()).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
124
test/moves/smack-down-thousand-arrows.test.ts
Normal file
124
test/moves/smack-down-thousand-arrows.test.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Smack Down and Thousand Arrows", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleStyle("single")
|
||||||
|
.enemySpecies(SpeciesId.EELEKTROSS)
|
||||||
|
.startingLevel(100)
|
||||||
|
.enemyLevel(50)
|
||||||
|
.criticalHits(false)
|
||||||
|
.ability(AbilityId.COMPOUND_EYES)
|
||||||
|
.enemyAbility(AbilityId.STURDY)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: "Smack Down", move: MoveId.SMACK_DOWN },
|
||||||
|
{ name: "Thousand Arrows", move: MoveId.THOUSAND_ARROWS },
|
||||||
|
])("$name should hit and ground ungrounded targets", async ({ move }) => {
|
||||||
|
game.override.enemySpecies(SpeciesId.TORNADUS);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||||
|
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
expect(enemy.isGrounded()).toBe(false);
|
||||||
|
|
||||||
|
game.move.use(move);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
expect(enemy.isGrounded()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should affect targets with Levitate", async () => {
|
||||||
|
game.override.enemyPassiveAbility(AbilityId.LEVITATE);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||||
|
|
||||||
|
const eelektross = game.field.getEnemyPokemon();
|
||||||
|
expect(eelektross.isGrounded()).toBe(false);
|
||||||
|
|
||||||
|
game.move.use(MoveId.THOUSAND_ARROWS);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(eelektross).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||||
|
expect(eelektross.isGrounded()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: "TELEKINESIS", tag: BattlerTagType.TELEKINESIS },
|
||||||
|
{ name: "FLOATING", tag: BattlerTagType.FLOATING },
|
||||||
|
])("should cancel the effects of BattlerTagType.$name", async ({ tag }) => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||||
|
|
||||||
|
const eelektross = game.field.getEnemyPokemon();
|
||||||
|
eelektross.addTag(tag);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SMACK_DOWN);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(eelektross).not.toHaveBattlerTag(tag);
|
||||||
|
expect(eelektross).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
});
|
||||||
|
|
||||||
|
// NB: This test might sound useless, but semi-invulnerable pokemon are technically considered "ungrounded"
|
||||||
|
// by most things
|
||||||
|
it("should not ground semi-invulnerable targets hit via No Guard unless already ungrounded", async () => {
|
||||||
|
game.override.ability(AbilityId.NO_GUARD);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.THOUSAND_ARROWS);
|
||||||
|
await game.move.forceEnemyMove(MoveId.DIG);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
// Eelektross took damage but was not forcibly grounded
|
||||||
|
const eelektross = game.field.getEnemyPokemon();
|
||||||
|
expect(eelektross.isGrounded()).toBe(true);
|
||||||
|
expect(eelektross).not.toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
expect(eelektross.hp).toBeLessThan(eelektross.getMaxHp());
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Sky drop is currently partially implemented
|
||||||
|
it.todo("should hit midair targets from Sky Drop without interrupting");
|
||||||
|
|
||||||
|
describe("Thousand Arrows", () => {
|
||||||
|
it("should deal a fixed 1x damage to ungrounded flying-types", async () => {
|
||||||
|
game.override.enemySpecies(SpeciesId.ARCHEOPS);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
||||||
|
|
||||||
|
const archeops = game.field.getEnemyPokemon();
|
||||||
|
game.move.use(MoveId.THOUSAND_ARROWS);
|
||||||
|
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||||
|
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(hitSpy).toHaveReturnedWith([expect.anything(), 1]);
|
||||||
|
expect(archeops).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
expect(archeops.isGrounded()).toBe(true);
|
||||||
|
expect(archeops.hp).toBeLessThan(archeops.getMaxHp());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,10 +1,13 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
import { TerrainType } from "#app/data/terrain";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { HitCheckResult } from "#enums/hit-check-result";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import type { MoveEffectPhase } from "#phases/move-effect-phase";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@ -26,114 +29,117 @@ describe("Moves - Telekinesis", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.TELEKINESIS, MoveId.TACKLE, MoveId.MUD_SHOT, MoveId.SMACK_DOWN])
|
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.enemySpecies(SpeciesId.SNORLAX)
|
.enemySpecies(SpeciesId.SNORLAX)
|
||||||
.enemyLevel(60)
|
.enemyLevel(60)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.enemyMoveset([MoveId.SPLASH]);
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Telekinesis makes the affected vulnerable to most attacking moves regardless of accuracy", async () => {
|
it("should cause opposing non-OHKO moves to always hit the target", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
vi.spyOn(allMoves[MoveId.TACKLE], "accuracy", "get").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.TACKLE);
|
expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
expect(enemy).toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||||
expect(enemyOpponent.isFullHp()).toBe(false);
|
|
||||||
|
game.move.use(MoveId.TACKLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.move.forceMiss();
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||||
|
expect(player.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Telekinesis makes the affected airborne and immune to most Ground-moves", async () => {
|
it("should forcibly unground the target", async () => {
|
||||||
|
game.override.terrain(TerrainType.ELECTRIC);
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.toNextTurn();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
// Use Earthquake - should be ineffective
|
||||||
|
game.move.use(MoveId.EARTHQUAKE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||||
|
const hitSpy = vi.spyOn(game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(100);
|
|
||||||
game.move.select(MoveId.MUD_SHOT);
|
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
expect(hitSpy).toHaveLastReturnedWith([HitCheckResult.NO_EFFECT, 0]);
|
||||||
expect(enemyOpponent.isFullHp()).toBe(true);
|
|
||||||
|
// Use Spore - should succeed due to being ungrounded
|
||||||
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Telekinesis can still affect Pokemon that have been transformed into invalid Pokemon", async () => {
|
// TODO: Make an it.each testing the invalid species for Telekinesis
|
||||||
game.override.enemyMoveset(MoveId.TRANSFORM);
|
it.todo.each([])("should fail if used on $name", () => {});
|
||||||
|
|
||||||
|
it("should still affect enemies transformed into invalid Pokemon", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.IMPOSTER);
|
||||||
await game.classicMode.startBattle([SpeciesId.DIGLETT]);
|
await game.classicMode.startBattle([SpeciesId.DIGLETT]);
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const enemyOpponent = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
await game.toNextTurn();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
|
||||||
|
expect(enemyOpponent).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
|
expect(enemyOpponent).toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||||
expect(enemyOpponent.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT);
|
expect(enemyOpponent.summonData.speciesForm?.speciesId).toBe(SpeciesId.DIGLETT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Moves like Smack Down and 1000 Arrows remove all effects of Telekinesis from the target Pokemon", async () => {
|
it("should become grounded when Ingrain is used, but not remove the guaranteed hit effect", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
|
||||||
|
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
|
||||||
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(MoveId.SMACK_DOWN);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Ingrain will remove the floating effect of Telekinesis, but not the 100% hit", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.SPLASH, MoveId.INGRAIN]);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
const enemyOpponent = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
game.move.select(MoveId.TELEKINESIS);
|
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeDefined();
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.TELEKINESIS);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
vi.spyOn(allMoves[MoveId.MUD_SHOT], "accuracy", "get").mockReturnValue(0);
|
|
||||||
game.move.select(MoveId.MUD_SHOT);
|
game.move.use(MoveId.MUD_SHOT);
|
||||||
await game.move.selectEnemyMove(MoveId.INGRAIN);
|
await game.move.forceEnemyMove(MoveId.INGRAIN);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.TELEKINESIS)).toBeDefined();
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.INGRAIN)).toBeDefined();
|
await game.move.forceMiss();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
await game.toEndOfTurn();
|
||||||
expect(enemyOpponent.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
|
||||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
expect(enemy).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.INGRAIN);
|
||||||
|
expect(enemy).toHaveBattlerTag(BattlerTagType.IGNORE_FLYING);
|
||||||
|
expect(enemy).not.toHaveBattlerTag(BattlerTagType.FLOATING);
|
||||||
|
expect(enemy.isGrounded()).toBe(true);
|
||||||
|
expect(playerPokemon).toHaveUsedMove({ move: MoveId.MUD_SHOT, result: MoveResult.SUCCESS });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not be baton passed onto a mega gengar", async () => {
|
it("should not be baton passable onto a mega gengar", async () => {
|
||||||
game.override
|
game.override.starterForms({ [SpeciesId.GENGAR]: 1 });
|
||||||
.moveset([MoveId.BATON_PASS])
|
|
||||||
.enemyMoveset([MoveId.TELEKINESIS])
|
|
||||||
.starterForms({ [SpeciesId.GENGAR]: 1 });
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GENGAR]);
|
||||||
game.move.select(MoveId.BATON_PASS);
|
|
||||||
|
game.move.use(MoveId.BATON_PASS);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.move.forceEnemyMove(MoveId.TELEKINESIS);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.phaseInterceptor.to("MoveEndPhase");
|
||||||
expect(game.field.getPlayerPokemon().getTag(BattlerTagType.TELEKINESIS)).toBeUndefined();
|
|
||||||
|
expect(game.field.getPlayerPokemon()).toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(game.field.getPlayerPokemon()).not.toHaveBattlerTag(BattlerTagType.TELEKINESIS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { BerryPhase } from "#phases/berry-phase";
|
|
||||||
import { MoveEffectPhase } from "#phases/move-effect-phase";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("Moves - Thousand Arrows", () => {
|
|
||||||
let phaserGame: Phaser.Game;
|
|
||||||
let game: GameManager;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
phaserGame = new Phaser.Game({
|
|
||||||
type: Phaser.HEADLESS,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
game.phaseInterceptor.restoreOg();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
game = new GameManager(phaserGame);
|
|
||||||
game.override
|
|
||||||
.battleStyle("single")
|
|
||||||
.enemySpecies(SpeciesId.TOGETIC)
|
|
||||||
.startingLevel(100)
|
|
||||||
.enemyLevel(100)
|
|
||||||
.moveset([MoveId.THOUSAND_ARROWS])
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("move should hit and ground Flying-type targets", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
|
||||||
// Enemy should not be grounded before move effect is applied
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("move should hit and ground targets with Levitate", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.SNORLAX).enemyAbility(AbilityId.LEVITATE);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
|
||||||
// Enemy should not be grounded before move effect is applied
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("move should hit and ground targets under the effects of Magnet Rise", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.SNORLAX);
|
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.ILLUMISE]);
|
|
||||||
|
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
|
||||||
|
|
||||||
enemyPokemon.addTag(BattlerTagType.FLOATING, undefined, MoveId.MAGNET_RISE);
|
|
||||||
|
|
||||||
game.move.select(MoveId.THOUSAND_ARROWS);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
|
||||||
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.FLOATING)).toBeUndefined();
|
|
||||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
|
||||||
});
|
|
||||||
});
|
|
@ -112,7 +112,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
|||||||
it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => {
|
it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => {
|
||||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||||
|
|
||||||
const pokemonPrior = scene.getPlayerParty().map(pokemon => pokemon);
|
const pokemonPrior = scene.getPlayerParty().slice();
|
||||||
const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal());
|
const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal());
|
||||||
|
|
||||||
await runMysteryEncounterToEnd(game, 1);
|
await runMysteryEncounterToEnd(game, 1);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,9 @@
|
|||||||
/** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
/** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
import type { NewArenaEvent } from "#events/battle-scene";
|
import type { NewArenaEvent } from "#events/battle-scene";
|
||||||
|
|
||||||
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||||
|
|
||||||
|
import { TerrainType } from "#app/data/terrain";
|
||||||
import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
|
import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -359,6 +361,19 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the starting {@linkcode TerrainType} that will be set on entering a new biome.
|
||||||
|
* @param type - The {@linkcode TerrainType} to set.
|
||||||
|
* @returns `this`
|
||||||
|
* @remarks
|
||||||
|
* The newly added terrain will be refreshed upon reaching a new biome, and will be overridden as normal if a new terrain is set.
|
||||||
|
*/
|
||||||
|
public terrain(type: TerrainType): this {
|
||||||
|
vi.spyOn(Overrides, "STARTING_TERRAIN_OVERRIDE", "get").mockReturnValue(type);
|
||||||
|
this.log(`Starting terrain for next biome set to ${TerrainType[type]} (=${type})!`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the seed
|
* Override the seed
|
||||||
* @param seed - The seed to set
|
* @param seed - The seed to set
|
||||||
|
@ -36,6 +36,7 @@ import { NewBiomeEncounterPhase } from "#phases/new-biome-encounter-phase";
|
|||||||
import { NextEncounterPhase } from "#phases/next-encounter-phase";
|
import { NextEncounterPhase } from "#phases/next-encounter-phase";
|
||||||
import { PartyExpPhase } from "#phases/party-exp-phase";
|
import { PartyExpPhase } from "#phases/party-exp-phase";
|
||||||
import { PartyHealPhase } from "#phases/party-heal-phase";
|
import { PartyHealPhase } from "#phases/party-heal-phase";
|
||||||
|
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||||
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
||||||
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
||||||
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
||||||
@ -146,6 +147,7 @@ export class PhaseInterceptor {
|
|||||||
[PositionalTagPhase, this.startPhase],
|
[PositionalTagPhase, this.startPhase],
|
||||||
[PokemonTransformPhase, this.startPhase],
|
[PokemonTransformPhase, this.startPhase],
|
||||||
[MysteryEncounterPhase, this.startPhase],
|
[MysteryEncounterPhase, this.startPhase],
|
||||||
|
[PokemonHealPhase, this.startPhase],
|
||||||
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
||||||
[MysteryEncounterBattlePhase, this.startPhase],
|
[MysteryEncounterBattlePhase, this.startPhase],
|
||||||
[MysteryEncounterRewardsPhase, this.startPhase],
|
[MysteryEncounterRewardsPhase, this.startPhase],
|
||||||
|
Loading…
Reference in New Issue
Block a user