mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-20 22:39:31 +02:00
Compare commits
13 Commits
cdc6c71e35
...
2380cc358c
Author | SHA1 | Date | |
---|---|---|---|
|
2380cc358c | ||
|
f42237d415 | ||
|
b44f0a4176 | ||
|
076ef81691 | ||
|
23271901cf | ||
|
1517e0512e | ||
|
7e402d02b0 | ||
|
371e99a4a2 | ||
|
13a4b99072 | ||
|
216018b409 | ||
|
7c60d0a5b1 | ||
|
f5e0ddd7af | ||
|
1f50ebdae0 |
@ -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) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -730,9 +727,7 @@ export class AttackTypeImmunityAbAttr extends TypeImmunityAbAttr {
|
|||||||
override canApply(params: TypeMultiplierAbAttrParams): boolean {
|
override canApply(params: TypeMultiplierAbAttrParams): boolean {
|
||||||
const { move } = params;
|
const { move } = params;
|
||||||
return (
|
return (
|
||||||
move.category !== MoveCategory.STATUS &&
|
move.category !== MoveCategory.STATUS && !move.hasAttr("VariableMoveTypeChartAttr") && super.canApply(params)
|
||||||
!move.hasAttr("NeutralDamageAgainstFlyingTypeMultiplierAttr") &&
|
|
||||||
super.canApply(params)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4191,71 +4186,43 @@ function getWeatherCondition(...weatherTypes: WeatherType[]): AbAttrCondition {
|
|||||||
if (globalScene.arena.weather?.isEffectSuppressed()) {
|
if (globalScene.arena.weather?.isEffectSuppressed()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const weatherType = globalScene.arena.weather?.weatherType;
|
return weatherTypes.includes(globalScene.arena.getWeatherType());
|
||||||
return !!weatherType && weatherTypes.indexOf(weatherType) > -1;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAnticipationCondition(): AbAttrCondition {
|
/**
|
||||||
return (pokemon: Pokemon) => {
|
* Condition used by {@linkcode AbilityId.ANTICIPATION} to show a message if any opponent knows a
|
||||||
for (const opponent of pokemon.getOpponents()) {
|
* "dangerous" move.
|
||||||
for (const move of opponent.moveset) {
|
* @param pokemon - The {@linkcode Pokemon} with this ability
|
||||||
// ignore null/undefined moves
|
* @returns Whether the message should be shown
|
||||||
if (!move) {
|
*/
|
||||||
continue;
|
const anticipationCondition: AbAttrCondition = (pokemon: Pokemon) =>
|
||||||
|
pokemon.getOpponents().some(opponent =>
|
||||||
|
opponent.moveset.some(movesetMove => {
|
||||||
|
// ignore null/undefined moves or non-attacks
|
||||||
|
const move = movesetMove?.getMove();
|
||||||
|
if (!move?.is("AttackMove")) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
// the move's base type (not accounting for variable type changes) is super effective
|
|
||||||
if (
|
if (move.hasAttr("OneHitKOAttr")) {
|
||||||
move.getMove().is("AttackMove") &&
|
|
||||||
pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// move is a OHKO
|
|
||||||
if (move.getMove().hasAttr("OneHitKOAttr")) {
|
// Check whether the move's base type (not accounting for variable type changes) is super effective
|
||||||
return true;
|
const type = new NumberHolder(
|
||||||
}
|
pokemon.getAttackTypeEffectiveness(move.type, {
|
||||||
// edge case for hidden power, type is computed
|
source: opponent,
|
||||||
if (move.getMove().id === MoveId.HIDDEN_POWER) {
|
ignoreStrongWinds: true,
|
||||||
const iv_val = Math.floor(
|
move: move,
|
||||||
(((opponent.ivs[Stat.HP] & 1) +
|
}),
|
||||||
(opponent.ivs[Stat.ATK] & 1) * 2 +
|
|
||||||
(opponent.ivs[Stat.DEF] & 1) * 4 +
|
|
||||||
(opponent.ivs[Stat.SPD] & 1) * 8 +
|
|
||||||
(opponent.ivs[Stat.SPATK] & 1) * 16 +
|
|
||||||
(opponent.ivs[Stat.SPDEF] & 1) * 32) *
|
|
||||||
15) /
|
|
||||||
63,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const type = [
|
// edge case for hidden power, type is computed
|
||||||
PokemonType.FIGHTING,
|
applyMoveAttrs("HiddenPowerTypeAttr", opponent, pokemon, move, type);
|
||||||
PokemonType.FLYING,
|
return type.value >= 2;
|
||||||
PokemonType.POISON,
|
}),
|
||||||
PokemonType.GROUND,
|
);
|
||||||
PokemonType.ROCK,
|
|
||||||
PokemonType.BUG,
|
|
||||||
PokemonType.GHOST,
|
|
||||||
PokemonType.STEEL,
|
|
||||||
PokemonType.FIRE,
|
|
||||||
PokemonType.WATER,
|
|
||||||
PokemonType.GRASS,
|
|
||||||
PokemonType.ELECTRIC,
|
|
||||||
PokemonType.PSYCHIC,
|
|
||||||
PokemonType.ICE,
|
|
||||||
PokemonType.DRAGON,
|
|
||||||
PokemonType.DARK,
|
|
||||||
][iv_val];
|
|
||||||
|
|
||||||
if (pokemon.getAttackTypeEffectiveness(type, opponent) >= 2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an ability condition that causes the ability to fail if that ability
|
* Creates an ability condition that causes the ability to fail if that ability
|
||||||
@ -7086,7 +7053,7 @@ export function initAbilities() {
|
|||||||
.attr(PostFaintContactDamageAbAttr, 4)
|
.attr(PostFaintContactDamageAbAttr, 4)
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(AbilityId.ANTICIPATION, 4)
|
new Ability(AbilityId.ANTICIPATION, 4)
|
||||||
.conditionalAttr(getAnticipationCondition(), PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAnticipation", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })),
|
.conditionalAttr(anticipationCondition, PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAnticipation", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })),
|
||||||
new Ability(AbilityId.FOREWARN, 4)
|
new Ability(AbilityId.FOREWARN, 4)
|
||||||
.attr(ForewarnAbAttr),
|
.attr(ForewarnAbAttr),
|
||||||
new Ability(AbilityId.UNAWARE, 4)
|
new Ability(AbilityId.UNAWARE, 4)
|
||||||
|
@ -958,7 +958,7 @@ class StealthRockTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDamageHpRatio(pokemon: Pokemon): number {
|
getDamageHpRatio(pokemon: Pokemon): number {
|
||||||
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true);
|
const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, { ignoreStrongWinds: true });
|
||||||
|
|
||||||
let damageHpRatio = 0;
|
let damageHpRatio = 0;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ import { StatusEffect } from "#enums/status-effect";
|
|||||||
import { SwitchType } from "#enums/switch-type";
|
import { SwitchType } from "#enums/switch-type";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { MoveUsedEvent } from "#events/battle-scene";
|
import { MoveUsedEvent } from "#events/battle-scene";
|
||||||
import type { EnemyPokemon, Pokemon } from "#field/pokemon";
|
import { EnemyPokemon, Pokemon } from "#field/pokemon";
|
||||||
import {
|
import {
|
||||||
AttackTypeBoosterModifier,
|
AttackTypeBoosterModifier,
|
||||||
BerryModifier,
|
BerryModifier,
|
||||||
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -999,7 +1005,7 @@ export class AttackMove extends Move {
|
|||||||
const ret = super.getTargetBenefitScore(user, target, move);
|
const ret = super.getTargetBenefitScore(user, target, move);
|
||||||
let attackScore = 0;
|
let attackScore = 0;
|
||||||
|
|
||||||
const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this);
|
const effectiveness = target.getAttackTypeEffectiveness(this.type, {source: user, move: this});
|
||||||
attackScore = Math.pow(effectiveness - 1, 2) * (effectiveness < 1 ? -2 : 2);
|
attackScore = Math.pow(effectiveness - 1, 2) * (effectiveness < 1 ? -2 : 2);
|
||||||
const [ thisStat, offStat ]: EffectiveStat[] = this.category === MoveCategory.PHYSICAL ? [ Stat.ATK, Stat.SPATK ] : [ Stat.SPATK, Stat.ATK ];
|
const [ thisStat, offStat ]: EffectiveStat[] = this.category === MoveCategory.PHYSICAL ? [ Stat.ATK, Stat.SPATK ] : [ Stat.SPATK, Stat.ATK ];
|
||||||
const statHolder = new NumberHolder(user.getEffectiveStat(thisStat, target));
|
const statHolder = new NumberHolder(user.getEffectiveStat(thisStat, target));
|
||||||
@ -1805,7 +1811,7 @@ export class SacrificialAttr extends MoveEffectAttr {
|
|||||||
if (user.isBoss()) {
|
if (user.isBoss()) {
|
||||||
return -20;
|
return -20;
|
||||||
}
|
}
|
||||||
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5));
|
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, {source: user}) - 0.5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1843,7 +1849,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
|
|||||||
if (user.isBoss()) {
|
if (user.isBoss()) {
|
||||||
return -20;
|
return -20;
|
||||||
}
|
}
|
||||||
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5));
|
return Math.ceil(((1 - user.getHpRatio()) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, {source: user}) - 0.5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1885,7 +1891,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
|
|||||||
if (user.isBoss()) {
|
if (user.isBoss()) {
|
||||||
return -10;
|
return -10;
|
||||||
}
|
}
|
||||||
return Math.ceil(((1 - user.getHpRatio() / 2) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, user) - 0.5));
|
return Math.ceil(((1 - user.getHpRatio() / 2) * 10 - 10) * (target.getAttackTypeEffectiveness(move.type, {source: user}) - 0.5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5359,86 +5365,80 @@ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VariableMoveTypeMultiplierAttr extends MoveAttr {
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
if (!target.getTag(BattlerTagType.IGNORE_FLYING)) {
|
|
||||||
const multiplier = args[0] as NumberHolder;
|
|
||||||
//When a flying type is hit, the first hit is always 1x multiplier.
|
|
||||||
if (target.isOfType(PokemonType.FLYING)) {
|
|
||||||
multiplier.value = 1;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr {
|
|
||||||
/**
|
|
||||||
* Checks to see if the Target is Ice-Type or not. If so, the move will have no effect.
|
|
||||||
* @param user n/a
|
|
||||||
* @param target The {@linkcode Pokemon} targeted by the move
|
|
||||||
* @param move n/a
|
|
||||||
* @param args `[0]` a {@linkcode NumberHolder | NumberHolder} containing a type effectiveness multiplier
|
|
||||||
* @returns `true` if this Ice-type immunity applies; `false` otherwise
|
|
||||||
*/
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
const multiplier = args[0] as NumberHolder;
|
|
||||||
if (target.isOfType(PokemonType.ICE)) {
|
|
||||||
multiplier.value = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
const multiplier = args[0] as NumberHolder;
|
|
||||||
multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, user);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute for moves which have a custom type chart interaction.
|
* Attribute for moves which have a custom type chart interaction.
|
||||||
*/
|
*/
|
||||||
export class VariableMoveTypeChartAttr extends MoveAttr {
|
export class VariableMoveTypeChartAttr extends MoveAttr {
|
||||||
/**
|
/**
|
||||||
* @param user {@linkcode Pokemon} using the move
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
* @param target {@linkcode Pokemon} target of the move
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
* @param move {@linkcode Move} with this attribute
|
* @param move - The {@linkcode Move} with this attribute
|
||||||
* @param args [0] {@linkcode NumberHolder} holding the type effectiveness
|
* @param args -
|
||||||
* @param args [1] A single defensive type of the target
|
* `[0]`: A {@linkcode NumberHolder} holding the type effectiveness
|
||||||
*
|
* `[1]`: The target's entire defensive type profile
|
||||||
* @returns true if application of the attribute succeeds
|
* `[2]`: The current {@linkcode PokemonType} of the move
|
||||||
|
* @returns `true` if application of the attribute succeeds
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class forces Freeze-Dry to be super effective against Water Type.
|
* Attribute to implement {@linkcode MoveId.FREEZE_DRY}'s guaranteed water type super effectiveness.
|
||||||
*/
|
*/
|
||||||
export class FreezeDryAttr extends VariableMoveTypeChartAttr {
|
export class FreezeDryAttr extends VariableMoveTypeChartAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||||
const multiplier = args[0] as NumberHolder;
|
const [multiplier, types, moveType] = args;
|
||||||
const defType = args[1] as PokemonType;
|
if (!types.includes(PokemonType.WATER)) {
|
||||||
|
|
||||||
if (defType === PokemonType.WATER) {
|
|
||||||
multiplier.value = 2;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace whatever the prior "normal" water effectiveness was with a guaranteed 2x multi
|
||||||
|
const normalEff = getTypeDamageMultiplier(moveType, PokemonType.WATER)
|
||||||
|
multiplier.value = 2 * multiplier.value / normalEff;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute used by {@linkcode MoveId.THOUSAND_ARROWS} to cause it to deal a fixed 1x damage
|
||||||
|
* against all ungrounded flying types.
|
||||||
|
*/
|
||||||
|
export class NeutralDamageAgainstFlyingTypeAttr extends VariableMoveTypeChartAttr {
|
||||||
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||||
|
const [multiplier, types] = args;
|
||||||
|
if (target.isGrounded() || !types.includes(PokemonType.FLYING)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
multiplier.value = 1;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute used by {@linkcode MoveId.FLYING_PRESS} to add the Flying Type to its type effectiveness.
|
||||||
|
*/
|
||||||
|
export class FlyingTypeMultiplierAttr extends VariableMoveTypeChartAttr {
|
||||||
|
apply(user: Pokemon, target: Pokemon, _move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||||
|
const multiplier = args[0];
|
||||||
|
multiplier.value *= target.getAttackTypeEffectiveness(PokemonType.FLYING, {source: user});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute used by {@linkcode MoveId.SHEER_COLD} to implement its Gen VII+ ice ineffectiveness.
|
||||||
|
*/
|
||||||
|
export class IceNoEffectTypeAttr extends VariableMoveTypeChartAttr {
|
||||||
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [multiplier: NumberHolder, types: PokemonType[], moveType: PokemonType]): boolean {
|
||||||
|
const [multiplier, types] = args;
|
||||||
|
if (types.includes(PokemonType.ICE)) {
|
||||||
|
multiplier.value = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8126,7 +8126,9 @@ export class UpperHandCondition extends MoveCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HitsSameTypeAttr extends VariableMoveTypeMultiplierAttr {
|
// TODO: Does this need to extend from this?
|
||||||
|
// The only reason it might is to show ineffectiveness text but w/e
|
||||||
|
export class HitsSameTypeAttr extends VariableMoveTypeChartAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const multiplier = args[0] as NumberHolder;
|
const multiplier = args[0] as NumberHolder;
|
||||||
if (!user.getTypes(true).some(type => target.getTypes(true).includes(type))) {
|
if (!user.getTypes(true).some(type => target.getTypes(true).includes(type))) {
|
||||||
@ -8395,8 +8397,7 @@ const MoveAttrs = Object.freeze({
|
|||||||
TeraStarstormTypeAttr,
|
TeraStarstormTypeAttr,
|
||||||
MatchUserTypeAttr,
|
MatchUserTypeAttr,
|
||||||
CombinedPledgeTypeAttr,
|
CombinedPledgeTypeAttr,
|
||||||
VariableMoveTypeMultiplierAttr,
|
NeutralDamageAgainstFlyingTypeAttr,
|
||||||
NeutralDamageAgainstFlyingTypeMultiplierAttr,
|
|
||||||
IceNoEffectTypeAttr,
|
IceNoEffectTypeAttr,
|
||||||
FlyingTypeMultiplierAttr,
|
FlyingTypeMultiplierAttr,
|
||||||
VariableMoveTypeChartAttr,
|
VariableMoveTypeChartAttr,
|
||||||
@ -10440,7 +10441,7 @@ export function initMoves() {
|
|||||||
.attr(HitHealAttr, 0.75)
|
.attr(HitHealAttr, 0.75)
|
||||||
.triageMove(),
|
.triageMove(),
|
||||||
new AttackMove(MoveId.THOUSAND_ARROWS, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
new AttackMove(MoveId.THOUSAND_ARROWS, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||||
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
.attr(NeutralDamageAgainstFlyingTypeAttr)
|
||||||
.attr(FallDownAttr)
|
.attr(FallDownAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
.attr(HitsTagAttr, BattlerTagType.FLOATING)
|
||||||
@ -11381,9 +11382,10 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.RUINATION, PokemonType.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9)
|
new AttackMove(MoveId.RUINATION, PokemonType.DARK, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 9)
|
||||||
.attr(TargetHalfHpDamageAttr),
|
.attr(TargetHalfHpDamageAttr),
|
||||||
new AttackMove(MoveId.COLLISION_COURSE, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
|
new AttackMove(MoveId.COLLISION_COURSE, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 9)
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461 / 4096 : 1),
|
// TODO: Do we want to change this to 4/3?
|
||||||
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, {source: user}) >= 2 ? 5461 / 4096 : 1),
|
||||||
new AttackMove(MoveId.ELECTRO_DRIFT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 9)
|
new AttackMove(MoveId.ELECTRO_DRIFT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 9)
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461 / 4096 : 1)
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, {source: user}) >= 2 ? 5461 / 4096 : 1)
|
||||||
.makesContact(),
|
.makesContact(),
|
||||||
new SelfStatusMove(MoveId.SHED_TAIL, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
new SelfStatusMove(MoveId.SHED_TAIL, PokemonType.NORMAL, -1, 10, -1, 0, 9)
|
||||||
.attr(AddSubstituteAttr, 0.5, true)
|
.attr(AddSubstituteAttr, 0.5, true)
|
||||||
|
@ -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`),
|
||||||
|
@ -2,6 +2,13 @@ import { PokemonType } from "#enums/pokemon-type";
|
|||||||
|
|
||||||
export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8;
|
export type TypeDamageMultiplier = 0 | 0.125 | 0.25 | 0.5 | 1 | 2 | 4 | 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type effectiveness multiplier of one PokemonType against another.
|
||||||
|
* @param attackType - The {@linkcode PokemonType} of the attacker
|
||||||
|
* @param defType - The {@linkcode PokemonType} of the defender
|
||||||
|
* @returns The type damage multiplier between the two types;
|
||||||
|
* will be either `0`, `0.5`, `1` or `2`.
|
||||||
|
*/
|
||||||
export function getTypeDamageMultiplier(attackType: PokemonType, defType: PokemonType): TypeDamageMultiplier {
|
export function getTypeDamageMultiplier(attackType: PokemonType, defType: PokemonType): TypeDamageMultiplier {
|
||||||
if (attackType === PokemonType.UNKNOWN || defType === PokemonType.UNKNOWN) {
|
if (attackType === PokemonType.UNKNOWN || defType === PokemonType.UNKNOWN) {
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,8 @@ import {
|
|||||||
TempStatStageBoosterModifier,
|
TempStatStageBoosterModifier,
|
||||||
} from "#modifiers/modifier";
|
} from "#modifiers/modifier";
|
||||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||||
import type { Move } from "#moves/move";
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { Move, VariableMoveTypeChartAttr } from "#moves/move";
|
||||||
import { getMoveTargets } from "#moves/move-utils";
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import { loadMoveAnimations } from "#sprites/pokemon-asset-loader";
|
import { loadMoveAnimations } from "#sprites/pokemon-asset-loader";
|
||||||
@ -205,6 +206,38 @@ type getBaseDamageParams = Omit<damageParams, "effectiveness">;
|
|||||||
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
|
/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */
|
||||||
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
|
type getAttackDamageParams = Omit<damageParams, "moveCategory">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for the parameters of {@linkcode Pokemon.getAttackTypeEffectiveness | getAttackTypeEffectiveness}
|
||||||
|
* and associated helper functions.
|
||||||
|
*/
|
||||||
|
type getAttackTypeEffectivenessParams = {
|
||||||
|
/**
|
||||||
|
* The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities
|
||||||
|
* and the effects of Foresight/Odor Sleuth.
|
||||||
|
*/
|
||||||
|
source?: Pokemon;
|
||||||
|
/**
|
||||||
|
* If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks)
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
ignoreStrongWinds?: boolean;
|
||||||
|
/**
|
||||||
|
* If `true`, will prevent changes to game state during calculations.
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
simulated?: boolean;
|
||||||
|
/**
|
||||||
|
* The {@linkcode Move} whose type effectiveness is being checked.
|
||||||
|
* Used for applying {@linkcode VariableMoveTypeChartAttr}
|
||||||
|
*/
|
||||||
|
move?: Move;
|
||||||
|
/**
|
||||||
|
* Whether to consider this Pokemon's {@linkcode IllusionData | illusion} when determining types.
|
||||||
|
* @defaultValue `false`
|
||||||
|
*/
|
||||||
|
useIllusion?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class Pokemon extends Phaser.GameObjects.Container {
|
export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
/**
|
/**
|
||||||
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
|
* This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID},
|
||||||
@ -2397,11 +2430,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
const typeMultiplier = new NumberHolder(
|
const typeMultiplier = new NumberHolder(
|
||||||
move.category !== MoveCategory.STATUS || move.hasAttr("RespectAttackTypeImmunityAttr")
|
move.category !== MoveCategory.STATUS || move.hasAttr("RespectAttackTypeImmunityAttr")
|
||||||
? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move, useIllusion)
|
? this.getAttackTypeEffectiveness(moveType, { source, simulated, move, useIllusion })
|
||||||
: 1,
|
: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
applyMoveAttrs("VariableMoveTypeMultiplierAttr", source, this, move, typeMultiplier);
|
|
||||||
if (this.getTypes(true, true).find(t => move.isTypeImmune(source, this, t))) {
|
if (this.getTypes(true, true).find(t => move.isTypeImmune(source, this, t))) {
|
||||||
typeMultiplier.value = 0;
|
typeMultiplier.value = 0;
|
||||||
}
|
}
|
||||||
@ -2461,26 +2493,31 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the move's type effectiveness multiplier based on the target's type/s.
|
* Calculate the type effectiveness multiplier of a Move used **against** this Pokemon.
|
||||||
* @param moveType {@linkcode PokemonType} the type of the move being used
|
* @param moveType - The {@linkcode PokemonType} of the move being used
|
||||||
* @param source {@linkcode Pokemon} the Pokemon using the move
|
* @param source - The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities
|
||||||
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
* and the effects of Foresight/Odor Sleuth
|
||||||
* @param simulated tag to only apply the strong winds effect message when the move is used
|
* @param ignoreStrongWinds - If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks);
|
||||||
* @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
|
* default `false`
|
||||||
* @param useIllusion - Whether we want the attack type effectiveness on the illusion or not
|
* @param simulated - If `true`, will prevent changes to game state during calculations; default `false`
|
||||||
* @returns a multiplier for the type effectiveness
|
* @param move - The {@linkcode Move} whose type effectiveness is being checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
|
||||||
|
* @param useIllusion - Whether to consider this Pokemon's {@linkcode IllusionData | illusion} when determining types; default `false`
|
||||||
|
* @returns The computed type effectiveness multiplier.
|
||||||
*/
|
*/
|
||||||
getAttackTypeEffectiveness(
|
getAttackTypeEffectiveness(
|
||||||
moveType: PokemonType,
|
moveType: PokemonType,
|
||||||
source?: Pokemon,
|
{
|
||||||
|
source,
|
||||||
ignoreStrongWinds = false,
|
ignoreStrongWinds = false,
|
||||||
simulated = true,
|
simulated = true,
|
||||||
move?: Move,
|
move,
|
||||||
useIllusion = false,
|
useIllusion = false,
|
||||||
|
}: getAttackTypeEffectivenessParams = {},
|
||||||
): TypeDamageMultiplier {
|
): TypeDamageMultiplier {
|
||||||
if (moveType === PokemonType.STELLAR) {
|
if (moveType === PokemonType.STELLAR) {
|
||||||
return this.isTerastallized ? 2 : 1;
|
return this.isTerastallized ? 2 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const types = this.getTypes(true, true, undefined, useIllusion);
|
const types = this.getTypes(true, true, undefined, useIllusion);
|
||||||
const arena = globalScene.arena;
|
const arena = globalScene.arena;
|
||||||
|
|
||||||
@ -2493,16 +2530,71 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let multiplier = types
|
const multi = new NumberHolder(1);
|
||||||
.map(defenderType => {
|
for (const defenderType of types) {
|
||||||
const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
const typeMulti = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
||||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, typeMulti);
|
||||||
if (move) {
|
// If the target is immune to the type in question, check for any effects that would ignore said effect
|
||||||
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defenderType);
|
// TODO: Review if the `isActive` check is needed anymore
|
||||||
|
if (
|
||||||
|
source?.isActive(true) &&
|
||||||
|
typeMulti.value === 0 &&
|
||||||
|
this.checkIgnoreTypeImmunity({ source, simulated, moveType, defenderType })
|
||||||
|
) {
|
||||||
|
typeMulti.value = 1;
|
||||||
}
|
}
|
||||||
if (source) {
|
multi.value *= typeMulti.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply any typing changes from Freeze-Dry, etc.
|
||||||
|
if (move) {
|
||||||
|
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multi, types, moveType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
||||||
|
const typeMultiplierAgainstFlying = new NumberHolder(getTypeDamageMultiplier(moveType, PokemonType.FLYING));
|
||||||
|
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, typeMultiplierAgainstFlying);
|
||||||
|
if (
|
||||||
|
!ignoreStrongWinds &&
|
||||||
|
arena.getWeatherType() === WeatherType.STRONG_WINDS &&
|
||||||
|
!arena.weather?.isEffectSuppressed() &&
|
||||||
|
types.includes(PokemonType.FLYING) &&
|
||||||
|
typeMultiplierAgainstFlying.value === 2
|
||||||
|
) {
|
||||||
|
multi.value /= 2;
|
||||||
|
if (!simulated) {
|
||||||
|
globalScene.phaseManager.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return multi.value as TypeDamageMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-method of {@linkcode getAttackTypeEffectiveness} that handles nullifying type immunities.
|
||||||
|
* @param source - The {@linkcode Pokemon} from whom the attack is sourced
|
||||||
|
* @param simulated - If `true`, will prevent displaying messages upon activation
|
||||||
|
* @param moveType - The {@linkcode PokemonType} whose offensive typing is being checked
|
||||||
|
* @param defenderType - The defender's {@linkcode PokemonType} being checked
|
||||||
|
* @returns Whether the type immunity was bypassed
|
||||||
|
*/
|
||||||
|
private checkIgnoreTypeImmunity({
|
||||||
|
source,
|
||||||
|
simulated,
|
||||||
|
moveType,
|
||||||
|
defenderType,
|
||||||
|
}: {
|
||||||
|
source: Pokemon;
|
||||||
|
simulated: boolean;
|
||||||
|
moveType: PokemonType;
|
||||||
|
defenderType: PokemonType;
|
||||||
|
}): boolean {
|
||||||
|
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
|
||||||
|
const hasExposed = exposedTags.some(t => t.ignoreImmunity(defenderType, moveType));
|
||||||
|
if (hasExposed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const ignoreImmunity = new BooleanHolder(false);
|
const ignoreImmunity = new BooleanHolder(false);
|
||||||
if (source.isActive(true) && source.hasAbilityWithAttr("IgnoreTypeImmunityAbAttr")) {
|
|
||||||
applyAbAttrs("IgnoreTypeImmunityAbAttr", {
|
applyAbAttrs("IgnoreTypeImmunityAbAttr", {
|
||||||
pokemon: source,
|
pokemon: source,
|
||||||
cancelled: ignoreImmunity,
|
cancelled: ignoreImmunity,
|
||||||
@ -2510,40 +2602,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
moveType,
|
moveType,
|
||||||
defenderType,
|
defenderType,
|
||||||
});
|
});
|
||||||
}
|
return ignoreImmunity.value;
|
||||||
if (ignoreImmunity.value) {
|
|
||||||
if (multiplier.value === 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
|
|
||||||
if (exposedTags.some(t => t.ignoreImmunity(defenderType, moveType))) {
|
|
||||||
if (multiplier.value === 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return multiplier.value;
|
|
||||||
})
|
|
||||||
.reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
|
|
||||||
|
|
||||||
const typeMultiplierAgainstFlying = new NumberHolder(getTypeDamageMultiplier(moveType, PokemonType.FLYING));
|
|
||||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, typeMultiplierAgainstFlying);
|
|
||||||
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
|
||||||
if (
|
|
||||||
!ignoreStrongWinds &&
|
|
||||||
arena.weather?.weatherType === WeatherType.STRONG_WINDS &&
|
|
||||||
!arena.weather.isEffectSuppressed() &&
|
|
||||||
this.isOfType(PokemonType.FLYING) &&
|
|
||||||
typeMultiplierAgainstFlying.value === 2
|
|
||||||
) {
|
|
||||||
multiplier /= 2;
|
|
||||||
if (!simulated) {
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return multiplier as TypeDamageMultiplier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2563,10 +2622,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* Based on how effectively this Pokemon defends against the opponent's types.
|
* Based on how effectively this Pokemon defends against the opponent's types.
|
||||||
* This score cannot be higher than 4.
|
* This score cannot be higher than 4.
|
||||||
*/
|
*/
|
||||||
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], opponent), 0.25);
|
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], { source: opponent }), 0.25);
|
||||||
if (enemyTypes.length > 1) {
|
if (enemyTypes.length > 1) {
|
||||||
defScore *=
|
defScore *=
|
||||||
1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25);
|
// TODO: Shouldn't this pass `simulated=true` here?
|
||||||
|
1 /
|
||||||
|
Math.max(
|
||||||
|
this.getAttackTypeEffectiveness(enemyTypes[1], { source: opponent, simulated: false, useIllusion: true }),
|
||||||
|
0.25,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveset = this.moveset;
|
const moveset = this.moveset;
|
||||||
@ -2580,7 +2644,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const moveType = resolvedMove.type;
|
const moveType = resolvedMove.type;
|
||||||
let thisScore = opponent.getAttackTypeEffectiveness(moveType, this, false, true, undefined, true);
|
let thisScore = opponent.getAttackTypeEffectiveness(moveType, {
|
||||||
|
source: this,
|
||||||
|
simulated: true,
|
||||||
|
useIllusion: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Add STAB multiplier for attack type effectiveness.
|
// Add STAB multiplier for attack type effectiveness.
|
||||||
// For now, simply don't apply STAB to moves that may change type
|
// For now, simply don't apply STAB to moves that may change type
|
||||||
|
@ -62,15 +62,24 @@ export class GameMode implements GameModeConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables challenges if they are disabled and sets the specified challenge's value
|
* Enables challenges if they are disabled and sets the specified challenge's value
|
||||||
* @param challenge The challenge to set
|
* @param challenge - The challenge to set
|
||||||
* @param value The value to give the challenge. Impact depends on the specific challenge
|
* @param value - The value to give the challenge. Impact depends on the specific challenge
|
||||||
|
* @param severity - If provided, will override the given severity amount. Unused if `challenge` does not use severity
|
||||||
|
* @todo Add severity support to daily mode challenge setting
|
||||||
*/
|
*/
|
||||||
setChallengeValue(challenge: Challenges, value: number) {
|
setChallengeValue(challenge: Challenges, value: number, severity?: number) {
|
||||||
if (!this.isChallenge) {
|
if (!this.isChallenge) {
|
||||||
this.isChallenge = true;
|
this.isChallenge = true;
|
||||||
this.challenges = allChallenges.map(c => copyChallenge(c));
|
this.challenges = allChallenges.map(c => copyChallenge(c));
|
||||||
}
|
}
|
||||||
this.challenges.filter((chal: Challenge) => chal.id === challenge).map((chal: Challenge) => (chal.value = value));
|
this.challenges
|
||||||
|
.filter((chal: Challenge) => chal.id === challenge)
|
||||||
|
.forEach(chal => {
|
||||||
|
chal.value = value;
|
||||||
|
if (chal.hasSeverity()) {
|
||||||
|
chal.severity = severity ?? chal.severity;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)!)
|
||||||
|
: {};
|
||||||
|
|
||||||
Object.keys(Tutorial)
|
// TODO: We shouldn't be storing this like that
|
||||||
.map(t => t as Tutorial)
|
for (const key of Object.values(Tutorial)) {
|
||||||
.forEach(t => {
|
|
||||||
const key = Tutorial[t];
|
|
||||||
if (key === tutorial) {
|
if (key === tutorial) {
|
||||||
tutorials[key] = flag;
|
tutorials[key] = status;
|
||||||
} else {
|
} else {
|
||||||
tutorials[key] ??= false;
|
tutorials[key] ??= false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
localStorage.setItem(key, JSON.stringify(tutorials));
|
localStorage.setItem(saveDataKey, 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) {
|
||||||
|
manageDataOptions.push({
|
||||||
|
label: i18next.t("menu:loadGame"),
|
||||||
|
handler: () => {
|
||||||
|
globalScene.ui.revertMode();
|
||||||
originalCallback?.(cursor);
|
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,20 +627,31 @@ 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
|
||||||
|
.getSession(this.slotId)
|
||||||
|
.then(async sessionData => {
|
||||||
// Ignore the results if the view was exited
|
// Ignore the results if the view was exited
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.hasData = !!sessionData;
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
this.hasData = false;
|
|
||||||
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty"));
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hasData = true;
|
|
||||||
this.saveData = sessionData;
|
this.saveData = sessionData;
|
||||||
await this.setupWithData(sessionData);
|
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);
|
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),
|
||||||
|
@ -88,6 +88,7 @@ describe("Abilities - Illusion", () => {
|
|||||||
expect(game.field.getPlayerPokemon().summonData.illusion).toBeFalsy();
|
expect(game.field.getPlayerPokemon().summonData.illusion).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: This doesn't actually check that the ai calls the function this way... useless test
|
||||||
it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => {
|
it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => {
|
||||||
game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]);
|
game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]);
|
||||||
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]);
|
||||||
@ -97,22 +98,16 @@ describe("Abilities - Illusion", () => {
|
|||||||
|
|
||||||
const flameThrower = enemy.getMoveset()[0]!.getMove();
|
const flameThrower = enemy.getMoveset()[0]!.getMove();
|
||||||
const psychic = enemy.getMoveset()[1]!.getMove();
|
const psychic = enemy.getMoveset()[1]!.getMove();
|
||||||
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(
|
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(flameThrower.type, {
|
||||||
flameThrower.type,
|
source: enemy,
|
||||||
enemy,
|
move: flameThrower,
|
||||||
undefined,
|
useIllusion: true,
|
||||||
undefined,
|
});
|
||||||
flameThrower,
|
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(psychic.type, {
|
||||||
true,
|
source: enemy,
|
||||||
);
|
move: psychic,
|
||||||
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(
|
useIllusion: true,
|
||||||
psychic.type,
|
});
|
||||||
enemy,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
psychic,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
|
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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 { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
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";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
@ -113,4 +114,18 @@ describe("Abilities - Tera Shell", () => {
|
|||||||
}
|
}
|
||||||
expect(spy).toHaveReturnedTimes(2);
|
expect(spy).toHaveReturnedTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should overwrite Freeze-Dry", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.TERAPAGOS]);
|
||||||
|
|
||||||
|
const terapagos = game.field.getPlayerPokemon();
|
||||||
|
terapagos.summonData.types = [PokemonType.WATER];
|
||||||
|
const spy = vi.spyOn(terapagos, "getMoveEffectiveness");
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FREEZE_DRY);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(spy).toHaveLastReturnedWith(0.5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -42,7 +42,7 @@ describe("Weather - Strong Winds", () => {
|
|||||||
game.move.select(MoveId.THUNDERBOLT);
|
game.move.select(MoveId.THUNDERBOLT);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
await game.phaseInterceptor.to(TurnStartPhase);
|
||||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, pikachu)).toBe(0.5);
|
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, { source: pikachu })).toBe(0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("electric type move is neutral for flying type pokemon", async () => {
|
it("electric type move is neutral for flying type pokemon", async () => {
|
||||||
@ -53,7 +53,7 @@ describe("Weather - Strong Winds", () => {
|
|||||||
game.move.select(MoveId.THUNDERBOLT);
|
game.move.select(MoveId.THUNDERBOLT);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
await game.phaseInterceptor.to(TurnStartPhase);
|
||||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, pikachu)).toBe(1);
|
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.THUNDERBOLT].type, { source: pikachu })).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ice type move is neutral for flying type pokemon", async () => {
|
it("ice type move is neutral for flying type pokemon", async () => {
|
||||||
@ -64,7 +64,7 @@ describe("Weather - Strong Winds", () => {
|
|||||||
game.move.select(MoveId.ICE_BEAM);
|
game.move.select(MoveId.ICE_BEAM);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
await game.phaseInterceptor.to(TurnStartPhase);
|
||||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ICE_BEAM].type, pikachu)).toBe(1);
|
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ICE_BEAM].type, { source: pikachu })).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rock type move is neutral for flying type pokemon", async () => {
|
it("rock type move is neutral for flying type pokemon", async () => {
|
||||||
@ -75,7 +75,7 @@ describe("Weather - Strong Winds", () => {
|
|||||||
game.move.select(MoveId.ROCK_SLIDE);
|
game.move.select(MoveId.ROCK_SLIDE);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
await game.phaseInterceptor.to(TurnStartPhase);
|
||||||
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ROCK_SLIDE].type, pikachu)).toBe(1);
|
expect(enemy.getAttackTypeEffectiveness(allMoves[MoveId.ROCK_SLIDE].type, { source: pikachu })).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("weather goes away when last trainer pokemon dies to indirect damage", async () => {
|
it("weather goes away when last trainer pokemon dies to indirect damage", async () => {
|
||||||
|
@ -106,21 +106,6 @@ describe("Inverse Battle", () => {
|
|||||||
expect(currentHp).toBeGreaterThan((maxHp * 31) / 32 - 1);
|
expect(currentHp).toBeGreaterThan((maxHp * 31) / 32 - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Freeze Dry is 2x effective against Water Type like other Ice type Move - Freeze Dry against Squirtle", async () => {
|
|
||||||
game.override.moveset([MoveId.FREEZE_DRY]).enemySpecies(SpeciesId.SQUIRTLE);
|
|
||||||
|
|
||||||
await game.challengeMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Water Absorb should heal against water moves - Water Absorb against Water gun", async () => {
|
it("Water Absorb should heal against water moves - Water Absorb against Water gun", async () => {
|
||||||
game.override.moveset([MoveId.WATER_GUN]).enemyAbility(AbilityId.WATER_ABSORB);
|
game.override.moveset([MoveId.WATER_GUN]).enemyAbility(AbilityId.WATER_ABSORB);
|
||||||
|
|
||||||
@ -164,6 +149,7 @@ describe("Inverse Battle", () => {
|
|||||||
expect(enemy.status?.effect).not.toBe(StatusEffect.PARALYSIS);
|
expect(enemy.status?.effect).not.toBe(StatusEffect.PARALYSIS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: These should belong to their respective moves' test files, not the inverse battle mechanic itself
|
||||||
it("Ground type is not immune to Thunder Wave - Thunder Wave against Sandshrew", async () => {
|
it("Ground type is not immune to Thunder Wave - Thunder Wave against Sandshrew", async () => {
|
||||||
game.override.moveset([MoveId.THUNDER_WAVE]).enemySpecies(SpeciesId.SANDSHREW);
|
game.override.moveset([MoveId.THUNDER_WAVE]).enemySpecies(SpeciesId.SANDSHREW);
|
||||||
|
|
||||||
@ -202,21 +188,6 @@ describe("Inverse Battle", () => {
|
|||||||
expect(player.getTypes()[0]).toBe(PokemonType.DRAGON);
|
expect(player.getTypes()[0]).toBe(PokemonType.DRAGON);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Flying Press should be 0.25x effective against Grass + Dark Type - Flying Press against Meowscarada", async () => {
|
|
||||||
game.override.moveset([MoveId.FLYING_PRESS]).enemySpecies(SpeciesId.MEOWSCARADA);
|
|
||||||
|
|
||||||
await game.challengeMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FLYING_PRESS);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(0.25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Scrappy ability has no effect - Tackle against Ghost Type still 2x effective with Scrappy", async () => {
|
it("Scrappy ability has no effect - Tackle against Ghost Type still 2x effective with Scrappy", async () => {
|
||||||
game.override.moveset([MoveId.TACKLE]).ability(AbilityId.SCRAPPY).enemySpecies(SpeciesId.GASTLY);
|
game.override.moveset([MoveId.TACKLE]).ability(AbilityId.SCRAPPY).enemySpecies(SpeciesId.GASTLY);
|
||||||
|
|
||||||
|
131
test/moves/flying-press.test.ts
Normal file
131
test/moves/flying-press.test.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { allAbilities, allMoves } from "#data/data-lists";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { Challenges } from "#enums/challenges";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { getEnumValues } from "#utils/enums";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe.sequential("Move - Flying Press", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
let hawlucha: PlayerPokemon;
|
||||||
|
let enemy: EnemyPokemon;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
|
.battleStyle("single")
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.HAWLUCHA]);
|
||||||
|
|
||||||
|
hawlucha = game.field.getPlayerPokemon();
|
||||||
|
enemy = game.field.getEnemyPokemon();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset temp data after each test
|
||||||
|
afterEach(() => {
|
||||||
|
hawlucha.resetSummonData();
|
||||||
|
enemy.resetSummonData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const pokemonTypes = getEnumValues(PokemonType);
|
||||||
|
|
||||||
|
function checkEffForAllTypes(primaryType: PokemonType) {
|
||||||
|
for (const type of pokemonTypes) {
|
||||||
|
enemy.summonData.types = [type];
|
||||||
|
const primaryEff = enemy.getAttackTypeEffectiveness(primaryType, { source: hawlucha });
|
||||||
|
const flyingEff = enemy.getAttackTypeEffectiveness(PokemonType.FLYING, { source: hawlucha });
|
||||||
|
const flyingPressEff = enemy.getAttackTypeEffectiveness(hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]), {
|
||||||
|
source: hawlucha,
|
||||||
|
move: allMoves[MoveId.FLYING_PRESS],
|
||||||
|
});
|
||||||
|
expect
|
||||||
|
.soft(
|
||||||
|
flyingPressEff,
|
||||||
|
`Flying Press effectiveness against ${toTitleCase(PokemonType[type])} was incorrect!` +
|
||||||
|
`\nExpected: ${flyingPressEff},` +
|
||||||
|
`\nActual: ${primaryEff * flyingEff} (=${primaryEff} * ${flyingEff})`,
|
||||||
|
)
|
||||||
|
.toBe(primaryEff * flyingEff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Normal -", () => {
|
||||||
|
it("should deal damage as a Fighting/Flying type move by default", async () => {
|
||||||
|
checkEffForAllTypes(PokemonType.FIGHTING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal damage as an Electric/Flying type move when Electrify is active", async () => {
|
||||||
|
hawlucha.addTag(BattlerTagType.ELECTRIFIED);
|
||||||
|
checkEffForAllTypes(PokemonType.ELECTRIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal damage as a Normal/Flying type move when Normalize is active", async () => {
|
||||||
|
hawlucha.setTempAbility(allAbilities[AbilityId.NORMALIZE]);
|
||||||
|
checkEffForAllTypes(PokemonType.NORMAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal 8x damage against a Normal/Ice type with Grass added", () => {
|
||||||
|
enemy.summonData.types = [PokemonType.NORMAL, PokemonType.ICE];
|
||||||
|
enemy.summonData.addedType = PokemonType.GRASS;
|
||||||
|
|
||||||
|
const moveType = hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]);
|
||||||
|
const flyingPressEff = enemy.getAttackTypeEffectiveness(moveType, {
|
||||||
|
source: hawlucha,
|
||||||
|
move: allMoves[MoveId.FLYING_PRESS],
|
||||||
|
});
|
||||||
|
expect(flyingPressEff).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Inverse Battle -", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
game.challengeMode.overrideGameWithChallenges(Challenges.INVERSE_BATTLE, 1, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal damage as a Fighting/Flying type move by default", async () => {
|
||||||
|
checkEffForAllTypes(PokemonType.FIGHTING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal damage as an Electric/Flying type move when Electrify is active", async () => {
|
||||||
|
hawlucha.addTag(BattlerTagType.ELECTRIFIED);
|
||||||
|
checkEffForAllTypes(PokemonType.ELECTRIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal damage as a Normal/Flying type move when Normalize is active", async () => {
|
||||||
|
hawlucha.setTempAbility(allAbilities[AbilityId.NORMALIZE]);
|
||||||
|
checkEffForAllTypes(PokemonType.NORMAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deal 0.125x damage against a Normal/Ice type with Grass added", () => {
|
||||||
|
enemy.summonData.types = [PokemonType.NORMAL, PokemonType.ICE];
|
||||||
|
enemy.summonData.addedType = PokemonType.GRASS;
|
||||||
|
|
||||||
|
const moveType = hawlucha.getMoveType(allMoves[MoveId.FLYING_PRESS]);
|
||||||
|
const flyingPressEff = enemy.getAttackTypeEffectiveness(moveType, {
|
||||||
|
source: hawlucha,
|
||||||
|
move: allMoves[MoveId.FLYING_PRESS],
|
||||||
|
});
|
||||||
|
expect(flyingPressEff).toBe(0.125);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,330 +1,140 @@
|
|||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import type { TypeDamageMultiplier } from "#data/type";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { Challenges } from "#enums/challenges";
|
import { Challenges } from "#enums/challenges";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { stringifyEnumArray } from "#test/test-utils/string-utils";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("Moves - Freeze-Dry", () => {
|
type typesArray = [PokemonType] | [PokemonType, PokemonType] | [PokemonType, PokemonType, PokemonType];
|
||||||
|
|
||||||
|
describe.sequential("Move - Freeze-Dry", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
let game: GameManager;
|
let game: GameManager;
|
||||||
beforeAll(() => {
|
let feebas: PlayerPokemon;
|
||||||
|
let enemy: EnemyPokemon;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
phaserGame = new Phaser.Game({
|
phaserGame = new Phaser.Game({
|
||||||
type: Phaser.HEADLESS,
|
type: Phaser.HEADLESS,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
game.phaseInterceptor.restoreOg();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
.enemyMoveset(MoveId.SPLASH)
|
||||||
.starterSpecies(SpeciesId.FEEBAS)
|
.ability(AbilityId.BALL_FETCH);
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.moveset([MoveId.FREEZE_DRY, MoveId.FORESTS_CURSE, MoveId.SOAK]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
feebas = game.field.getPlayerPokemon();
|
||||||
|
enemy = game.field.getEnemyPokemon();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 2x damage to pure water types", async () => {
|
// Reset temp data after each test
|
||||||
await game.classicMode.startBattle();
|
afterEach(() => {
|
||||||
|
feebas.resetSummonData();
|
||||||
const enemy = game.field.getEnemyPokemon();
|
enemy.resetSummonData();
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
enemy.isTerastallized = false;
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 4x damage to water/flying types", async () => {
|
afterAll(() => {
|
||||||
game.override.enemySpecies(SpeciesId.WINGULL);
|
game.phaseInterceptor.restoreOg();
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 1x damage to water/fire types", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.VOLCANION);
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Freeze drys forced super effectiveness should overwrite wonder guard
|
* Check that Freeze-Dry is the given effectiveness against the given type.
|
||||||
|
* @param types - The base {@linkcode PokemonType}s to set; will populate `addedType` if above 3
|
||||||
|
* @param multi - The expected {@linkcode TypeDamageMultiplier}
|
||||||
*/
|
*/
|
||||||
|
function expectEffectiveness(types: typesArray, multi: TypeDamageMultiplier): void {
|
||||||
|
enemy.summonData.types = types.slice(0, 2);
|
||||||
|
if (types[2] !== undefined) {
|
||||||
|
enemy.summonData.addedType = types[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveType = feebas.getMoveType(allMoves[MoveId.FREEZE_DRY]);
|
||||||
|
const eff = enemy.getAttackTypeEffectiveness(moveType, { source: feebas, move: allMoves[MoveId.FREEZE_DRY] });
|
||||||
|
expect(
|
||||||
|
eff,
|
||||||
|
`Freeze-dry effectiveness against ${stringifyEnumArray(PokemonType, types)} was ${eff} instead of ${multi}!`,
|
||||||
|
).toBe(multi);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Normal -", () => {
|
||||||
|
it.each<{ name: string; types: typesArray; eff: TypeDamageMultiplier }>([
|
||||||
|
{ name: "Pure Water", types: [PokemonType.WATER], eff: 2 },
|
||||||
|
{ name: "Water/Ground", types: [PokemonType.WATER, PokemonType.GROUND], eff: 4 },
|
||||||
|
{ name: "Water/Flying/Grass", types: [PokemonType.WATER, PokemonType.FLYING, PokemonType.GRASS], eff: 8 },
|
||||||
|
{ name: "Water/Fire", types: [PokemonType.WATER, PokemonType.FIRE], eff: 1 },
|
||||||
|
])("should be $effx effective against a $name-type opponent", ({ types, eff }) => {
|
||||||
|
expectEffectiveness(types, eff);
|
||||||
|
});
|
||||||
|
|
||||||
it("should deal 2x dmg against soaked wonder guard target", async () => {
|
it("should deal 2x dmg against soaked wonder guard target", async () => {
|
||||||
game.override
|
game.field.mockAbility(enemy, AbilityId.WONDER_GUARD);
|
||||||
.enemySpecies(SpeciesId.SHEDINJA)
|
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
|
||||||
.starterSpecies(SpeciesId.MAGIKARP)
|
|
||||||
.moveset([MoveId.SOAK, MoveId.FREEZE_DRY]);
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
expectEffectiveness([PokemonType.WATER], 2);
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.SOAK);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
|
||||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => {
|
it("should consider the target's Tera Type", async () => {
|
||||||
game.override.enemySpecies(SpeciesId.QUAGSIRE);
|
// Steel type terastallized into Water; 2x
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FORESTS_CURSE);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 2x damage to steel type terastallized into water", async () => {
|
|
||||||
game.override.enemySpecies(SpeciesId.SKARMORY);
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
enemy.teraType = PokemonType.WATER;
|
enemy.teraType = PokemonType.WATER;
|
||||||
enemy.isTerastallized = true;
|
enemy.isTerastallized = true;
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
expectEffectiveness([PokemonType.STEEL], 2);
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
// Water type terastallized into steel; 0.5x
|
||||||
|
enemy.teraType = PokemonType.STEEL;
|
||||||
|
expectEffectiveness([PokemonType.WATER], 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 0.5x damage to water type terastallized into fire", async () => {
|
it.each<{ name: string; types: typesArray; eff: TypeDamageMultiplier }>([
|
||||||
game.override.enemySpecies(SpeciesId.PELIPPER);
|
{ name: "Pure Water", types: [PokemonType.WATER], eff: 2 },
|
||||||
await game.classicMode.startBattle();
|
{ name: "Water/Ghost", types: [PokemonType.WATER, PokemonType.GHOST], eff: 0 },
|
||||||
|
])("should be $effx effective against a $name-type opponent with Normalize", ({ types, eff }) => {
|
||||||
const enemy = game.field.getEnemyPokemon();
|
game.field.mockAbility(feebas, AbilityId.NORMALIZE);
|
||||||
enemy.teraType = PokemonType.FIRE;
|
expectEffectiveness(types, eff);
|
||||||
enemy.isTerastallized = true;
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => {
|
it("should not stack with Electrify", async () => {
|
||||||
game.override.enemySpecies(SpeciesId.TERAPAGOS).enemyAbility(AbilityId.TERA_SHELL);
|
feebas.addTag(BattlerTagType.ELECTRIFIED);
|
||||||
await game.classicMode.startBattle();
|
expect(feebas.getMoveType(allMoves[MoveId.FREEZE_DRY])).toBe(PokemonType.ELECTRIC);
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
expectEffectiveness([PokemonType.WATER], 2);
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
});
|
||||||
|
|
||||||
game.move.select(MoveId.SOAK);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 2x damage to water type under Normalize", async () => {
|
describe("Inverse Battle -", () => {
|
||||||
game.override.ability(AbilityId.NORMALIZE);
|
beforeAll(() => {
|
||||||
await game.classicMode.startBattle();
|
game.challengeMode.overrideGameWithChallenges(Challenges.INVERSE_BATTLE, 1, 1);
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 0.25x damage to rock/steel type under Normalize", async () => {
|
it("should deal 2x damage to Water type", async () => {
|
||||||
game.override.ability(AbilityId.NORMALIZE).enemySpecies(SpeciesId.SHIELDON);
|
expectEffectiveness([PokemonType.WATER], 2);
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 0x damage to water/ghost type under Normalize", async () => {
|
it("should deal 2x damage to Water type under Normalize", async () => {
|
||||||
game.override.ability(AbilityId.NORMALIZE).enemySpecies(SpeciesId.JELLICENT);
|
game.field.mockAbility(feebas, AbilityId.NORMALIZE);
|
||||||
await game.classicMode.startBattle();
|
expectEffectiveness([PokemonType.WATER], 2);
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 2x damage to water type under Electrify", async () => {
|
it("should still deal 2x damage to Water type under Electrify", async () => {
|
||||||
game.override.enemyMoveset([MoveId.ELECTRIFY]);
|
feebas.addTag(BattlerTagType.ELECTRIFIED);
|
||||||
await game.classicMode.startBattle();
|
expectEffectiveness([PokemonType.WATER], 2);
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 4x damage to water/flying type under Electrify", async () => {
|
it("should deal 1x damage to Water/Flying type under Electrify", async () => {
|
||||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.GYARADOS);
|
feebas.addTag(BattlerTagType.ELECTRIFIED);
|
||||||
await game.classicMode.startBattle();
|
expectEffectiveness([PokemonType.WATER, PokemonType.FLYING], 1);
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deal 0x damage to water/ground type under Electrify", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.BARBOACH);
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.FLAPPLE);
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 2x damage to Water type during inverse battle", async () => {
|
|
||||||
game.override.moveset([MoveId.FREEZE_DRY]).enemySpecies(SpeciesId.MAGIKARP);
|
|
||||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
|
||||||
|
|
||||||
await game.challengeMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 2x damage to Water type during inverse battle under Normalize", async () => {
|
|
||||||
game.override.moveset([MoveId.FREEZE_DRY]).ability(AbilityId.NORMALIZE).enemySpecies(SpeciesId.MAGIKARP);
|
|
||||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
|
||||||
|
|
||||||
await game.challengeMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 2x damage to Water type during inverse battle under Electrify", async () => {
|
|
||||||
game.override.moveset([MoveId.FREEZE_DRY]).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset([MoveId.ELECTRIFY]);
|
|
||||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
|
||||||
|
|
||||||
await game.challengeMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => {
|
|
||||||
game.override.enemyMoveset([MoveId.ELECTRIFY]).enemySpecies(SpeciesId.GYARADOS);
|
|
||||||
|
|
||||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
|
||||||
|
|
||||||
await game.challengeMode.startBattle();
|
|
||||||
|
|
||||||
const enemy = game.field.getEnemyPokemon();
|
|
||||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
|
||||||
|
|
||||||
game.move.select(MoveId.FREEZE_DRY);
|
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -32,16 +32,25 @@ describe("Moves - Synchronoise", () => {
|
|||||||
.enemyMoveset(MoveId.SPLASH);
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should consider the user's tera type if it is terastallized", async () => {
|
// TODO: Write test
|
||||||
|
it.todo("should affect all opponents that share a type with the user");
|
||||||
|
|
||||||
|
it("should consider the user's Tera Type if it is Terastallized", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.BIDOOF]);
|
await game.classicMode.startBattle([SpeciesId.BIDOOF]);
|
||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
const enemyPokemon = game.field.getEnemyPokemon();
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
// force the player to be terastallized
|
|
||||||
playerPokemon.teraType = PokemonType.WATER;
|
playerPokemon.teraType = PokemonType.WATER;
|
||||||
playerPokemon.isTerastallized = true;
|
game.move.selectWithTera(MoveId.SYNCHRONOISE);
|
||||||
game.move.select(MoveId.SYNCHRONOISE);
|
await game.toEndOfTurn();
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
expect(enemyPokemon).not.toHaveFullHp();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Write test
|
||||||
|
it.todo("should fail if no opponents share a type with the user");
|
||||||
|
|
||||||
|
// TODO: Write test
|
||||||
|
it.todo("should fail if the user is typeless");
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -12,6 +12,8 @@ import { generateStarter } from "#test/test-utils/game-manager-utils";
|
|||||||
import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper";
|
import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper";
|
||||||
import { copyChallenge } from "data/challenge";
|
import { copyChallenge } from "data/challenge";
|
||||||
|
|
||||||
|
type challengeStub = { id: Challenges; value: number; severity: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to handle Challenge mode specifics
|
* Helper to handle Challenge mode specifics
|
||||||
*/
|
*/
|
||||||
@ -33,8 +35,9 @@ export class ChallengeModeHelper extends GameManagerHelper {
|
|||||||
* Runs the Challenge game to the summon phase.
|
* Runs the Challenge game to the summon phase.
|
||||||
* @param gameMode - Optional game mode to set.
|
* @param gameMode - Optional game mode to set.
|
||||||
* @returns A promise that resolves when the summon phase is reached.
|
* @returns A promise that resolves when the summon phase is reached.
|
||||||
|
* @todo this duplicates nearly all its code with the classic mode variant...
|
||||||
*/
|
*/
|
||||||
async runToSummon(species?: SpeciesId[]) {
|
private async runToSummon(species?: SpeciesId[]) {
|
||||||
await this.game.runToTitle();
|
await this.game.runToTitle();
|
||||||
|
|
||||||
if (this.game.override.disableShinies) {
|
if (this.game.override.disableShinies) {
|
||||||
@ -88,4 +91,26 @@ export class ChallengeModeHelper extends GameManagerHelper {
|
|||||||
await this.game.phaseInterceptor.to(CommandPhase);
|
await this.game.phaseInterceptor.to(CommandPhase);
|
||||||
console.log("==================[New Turn]==================");
|
console.log("==================[New Turn]==================");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override an already-started game with the given challenges.
|
||||||
|
* @param id - The challenge id
|
||||||
|
* @param value - The challenge value
|
||||||
|
* @param severity - The challenge severity
|
||||||
|
* @todo Make severity optional for challenges that do not require it
|
||||||
|
*/
|
||||||
|
public overrideGameWithChallenges(id: Challenges, value: number, severity: number): void;
|
||||||
|
/**
|
||||||
|
* Override an already-started game with the given challenges.
|
||||||
|
* @param challenges - One or more challenges to set.
|
||||||
|
*/
|
||||||
|
public overrideGameWithChallenges(challenges: challengeStub[]): void;
|
||||||
|
public overrideGameWithChallenges(challenges: challengeStub[] | Challenges, value?: number, severity?: number): void {
|
||||||
|
if (typeof challenges !== "object") {
|
||||||
|
challenges = [{ id: challenges, value: value!, severity: severity! }];
|
||||||
|
}
|
||||||
|
for (const challenge of challenges) {
|
||||||
|
this.game.scene.gameMode.setChallengeValue(challenge.id, challenge.value, challenge.severity);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export class DailyModeHelper extends GameManagerHelper {
|
|||||||
* @returns A promise that resolves when the summon phase is reached.
|
* @returns A promise that resolves when the summon phase is reached.
|
||||||
* @remarks Please do not use for starting normal battles - use {@linkcode startBattle} instead
|
* @remarks Please do not use for starting normal battles - use {@linkcode startBattle} instead
|
||||||
*/
|
*/
|
||||||
async runToSummon(): Promise<void> {
|
private async runToSummon(): Promise<void> {
|
||||||
await this.game.runToTitle();
|
await this.game.runToTitle();
|
||||||
|
|
||||||
if (this.game.override.disableShinies) {
|
if (this.game.override.disableShinies) {
|
||||||
|
Loading…
Reference in New Issue
Block a user