mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-10 18:32:16 +02:00
Merge branch 'beta' into bug/776
This commit is contained in:
commit
241311d082
13
lefthook.yml
13
lefthook.yml
@ -2,6 +2,15 @@ pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
eslint:
|
||||
glob: '*.{js,jsx,ts,tsx}'
|
||||
glob: "*.{js,jsx,ts,tsx}"
|
||||
run: npx eslint --fix {staged_files}
|
||||
stage_fixed: true
|
||||
stage_fixed: true
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
||||
|
||||
pre-push:
|
||||
commands:
|
||||
eslint:
|
||||
glob: "*.{js,ts,jsx,tsx}"
|
||||
run: npx eslint --fix {push_files}
|
BIN
public/images/ui/legacy/settings_icon.png
Normal file
BIN
public/images/ui/legacy/settings_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 B |
BIN
public/images/ui/settings_icon.png
Normal file
BIN
public/images/ui/settings_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 B |
@ -6,7 +6,7 @@ import Trainer, { TrainerVariant } from "./field/trainer";
|
||||
import { GameMode } from "./game-mode";
|
||||
import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier";
|
||||
import { PokeballType } from "./data/pokeball";
|
||||
import {trainerConfigs} from "#app/data/trainer-config";
|
||||
import { trainerConfigs } from "#app/data/trainer-config";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattleSpec } from "#enums/battle-spec";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -31,7 +31,7 @@ export enum BattlerIndex {
|
||||
|
||||
export interface TurnCommand {
|
||||
command: Command;
|
||||
cursor?: integer;
|
||||
cursor?: number;
|
||||
move?: QueuedMove;
|
||||
targets?: BattlerIndex[];
|
||||
skip?: boolean;
|
||||
@ -39,38 +39,40 @@ export interface TurnCommand {
|
||||
}
|
||||
|
||||
interface TurnCommands {
|
||||
[key: integer]: TurnCommand | null
|
||||
[key: number]: TurnCommand | null
|
||||
}
|
||||
|
||||
export default class Battle {
|
||||
protected gameMode: GameMode;
|
||||
public waveIndex: integer;
|
||||
public waveIndex: number;
|
||||
public battleType: BattleType;
|
||||
public battleSpec: BattleSpec;
|
||||
public trainer: Trainer | null;
|
||||
public enemyLevels: integer[] | undefined;
|
||||
public enemyParty: EnemyPokemon[];
|
||||
public seenEnemyPartyMemberIds: Set<integer>;
|
||||
public enemyLevels: number[] | undefined;
|
||||
public enemyParty: EnemyPokemon[] = [];
|
||||
public seenEnemyPartyMemberIds: Set<number> = new Set<number>();
|
||||
public double: boolean;
|
||||
public started: boolean;
|
||||
public enemySwitchCounter: integer;
|
||||
public turn: integer;
|
||||
public started: boolean = false;
|
||||
public enemySwitchCounter: number = 0;
|
||||
public turn: number = 0;
|
||||
public turnCommands: TurnCommands;
|
||||
public playerParticipantIds: Set<integer>;
|
||||
public battleScore: integer;
|
||||
public postBattleLoot: PokemonHeldItemModifier[];
|
||||
public escapeAttempts: integer;
|
||||
public playerParticipantIds: Set<number> = new Set<number>();
|
||||
public battleScore: number = 0;
|
||||
public postBattleLoot: PokemonHeldItemModifier[] = [];
|
||||
public escapeAttempts: number = 0;
|
||||
public lastMove: Moves;
|
||||
public battleSeed: string;
|
||||
private battleSeedState: string | null;
|
||||
public moneyScattered: number;
|
||||
public lastUsedPokeball: PokeballType | null;
|
||||
public playerFaints: number; // The amount of times pokemon on the players side have fainted
|
||||
public enemyFaints: number; // The amount of times pokemon on the enemies side have fainted
|
||||
public battleSeed: string = Utils.randomString(16, true);
|
||||
private battleSeedState: string | null = null;
|
||||
public moneyScattered: number = 0;
|
||||
public lastUsedPokeball: PokeballType | null = null;
|
||||
/** The number of times a Pokemon on the player's side has fainted this battle */
|
||||
public playerFaints: number = 0;
|
||||
/** The number of times a Pokemon on the enemy's side has fainted this battle */
|
||||
public enemyFaints: number = 0;
|
||||
|
||||
private rngCounter: integer = 0;
|
||||
private rngCounter: number = 0;
|
||||
|
||||
constructor(gameMode: GameMode, waveIndex: integer, battleType: BattleType, trainer?: Trainer, double?: boolean) {
|
||||
constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) {
|
||||
this.gameMode = gameMode;
|
||||
this.waveIndex = waveIndex;
|
||||
this.battleType = battleType;
|
||||
@ -79,22 +81,7 @@ export default class Battle {
|
||||
this.enemyLevels = battleType !== BattleType.TRAINER
|
||||
? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave())
|
||||
: trainer?.getPartyLevels(this.waveIndex);
|
||||
this.enemyParty = [];
|
||||
this.seenEnemyPartyMemberIds = new Set<integer>();
|
||||
this.double = !!double;
|
||||
this.enemySwitchCounter = 0;
|
||||
this.turn = 0;
|
||||
this.playerParticipantIds = new Set<integer>();
|
||||
this.battleScore = 0;
|
||||
this.postBattleLoot = [];
|
||||
this.escapeAttempts = 0;
|
||||
this.started = false;
|
||||
this.battleSeed = Utils.randomString(16, true);
|
||||
this.battleSeedState = null;
|
||||
this.moneyScattered = 0;
|
||||
this.lastUsedPokeball = null;
|
||||
this.playerFaints = 0;
|
||||
this.enemyFaints = 0;
|
||||
this.double = double ?? false;
|
||||
}
|
||||
|
||||
private initBattleSpec(): void {
|
||||
@ -105,7 +92,7 @@ export default class Battle {
|
||||
this.battleSpec = spec;
|
||||
}
|
||||
|
||||
private getLevelForWave(): integer {
|
||||
private getLevelForWave(): number {
|
||||
const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex);
|
||||
const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2);
|
||||
const bossMultiplier = 1.2;
|
||||
@ -138,7 +125,7 @@ export default class Battle {
|
||||
return rand / value;
|
||||
}
|
||||
|
||||
getBattlerCount(): integer {
|
||||
getBattlerCount(): number {
|
||||
return this.double ? 2 : 1;
|
||||
}
|
||||
|
||||
@ -367,7 +354,7 @@ export default class Battle {
|
||||
return null;
|
||||
}
|
||||
|
||||
randSeedInt(scene: BattleScene, range: integer, min: integer = 0): integer {
|
||||
randSeedInt(scene: BattleScene, range: number, min: number = 0): number {
|
||||
if (range <= 1) {
|
||||
return min;
|
||||
}
|
||||
@ -392,7 +379,7 @@ export default class Battle {
|
||||
}
|
||||
|
||||
export class FixedBattle extends Battle {
|
||||
constructor(scene: BattleScene, waveIndex: integer, config: FixedBattleConfig) {
|
||||
constructor(scene: BattleScene, waveIndex: number, config: FixedBattleConfig) {
|
||||
super(scene.gameMode, waveIndex, config.battleType, config.battleType === BattleType.TRAINER ? config.getTrainer(scene) : undefined, config.double);
|
||||
if (config.getEnemyParty) {
|
||||
this.enemyParty = config.getEnemyParty(scene);
|
||||
@ -408,7 +395,7 @@ export class FixedBattleConfig {
|
||||
public double: boolean;
|
||||
public getTrainer: GetTrainerFunc;
|
||||
public getEnemyParty: GetEnemyPartyFunc;
|
||||
public seedOffsetWaveIndex: integer;
|
||||
public seedOffsetWaveIndex: number;
|
||||
|
||||
setBattleType(battleType: BattleType): FixedBattleConfig {
|
||||
this.battleType = battleType;
|
||||
@ -430,7 +417,7 @@ export class FixedBattleConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
setSeedOffsetWave(seedOffsetWaveIndex: integer): FixedBattleConfig {
|
||||
setSeedOffsetWave(seedOffsetWaveIndex: number): FixedBattleConfig {
|
||||
this.seedOffsetWaveIndex = seedOffsetWaveIndex;
|
||||
return this;
|
||||
}
|
||||
@ -476,7 +463,7 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], rand
|
||||
}
|
||||
|
||||
export interface FixedBattleConfigs {
|
||||
[key: integer]: FixedBattleConfig
|
||||
[key: number]: FixedBattleConfig
|
||||
}
|
||||
/**
|
||||
* Youngster/Lass on 5
|
||||
|
13
src/data/ability.ts
Normal file → Executable file
13
src/data/ability.ts
Normal file → Executable file
@ -1085,7 +1085,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||
}
|
||||
|
||||
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
||||
if (!attacker.summonData.disabledMove) {
|
||||
if (attacker.getTag(BattlerTagType.DISABLED) === null) {
|
||||
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
@ -1093,21 +1093,12 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||
|
||||
this.attacker = attacker;
|
||||
this.move = move;
|
||||
|
||||
attacker.summonData.disabledMove = move.id;
|
||||
attacker.summonData.disabledTurns = 4;
|
||||
this.attacker.addTag(BattlerTagType.DISABLED, 4, 0, pokemon.id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
|
||||
return i18next.t("abilityTriggers:postDefendMoveDisable", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.attacker),
|
||||
moveName: this.move.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr {
|
||||
|
@ -98,6 +98,127 @@ export interface TerrainBattlerTag {
|
||||
terrainTypes: TerrainType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move
|
||||
* in-game. This is not to be confused with {@linkcode Moves.DISABLE}.
|
||||
*
|
||||
* Descendants can override {@linkcode isMoveRestricted} to restrict moves that
|
||||
* match a condition. A restricted move gets cancelled before it is used. Players and enemies should not be allowed
|
||||
* to select restricted moves.
|
||||
*/
|
||||
export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
constructor(tagType: BattlerTagType, turnCount: integer, sourceMove?: Moves, sourceId?: integer) {
|
||||
super(tagType, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], turnCount, sourceMove, sourceId);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
// Cancel the affected pokemon's selected move
|
||||
const phase = pokemon.scene.getCurrentPhase() as MovePhase;
|
||||
const move = phase.move;
|
||||
|
||||
if (this.isMoveRestricted(move.moveId)) {
|
||||
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
|
||||
phase.cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this tag is restricting a move.
|
||||
*
|
||||
* @param {Moves} move {@linkcode Moves} ID to check restriction for.
|
||||
* @returns {boolean} `true` if the move is restricted by this tag, otherwise `false`.
|
||||
*/
|
||||
abstract isMoveRestricted(move: Moves): boolean;
|
||||
|
||||
/**
|
||||
* Gets the text to display when the player attempts to select a move that is restricted by this tag.
|
||||
*
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} for which the player is attempting to select the restricted move
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move that is having its selection denied
|
||||
* @returns {string} text to display when the player attempts to select the restricted move
|
||||
*/
|
||||
abstract selectionDeniedText(pokemon: Pokemon, move: Moves): string;
|
||||
|
||||
/**
|
||||
* Gets the text to display when a move's execution is prevented as a result of the restriction.
|
||||
* Because restriction effects also prevent selection of the move, this situation can only arise if a
|
||||
* pokemon first selects a move, then gets outsped by a pokemon using a move that restricts the selected move.
|
||||
*
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
|
||||
* @returns {string} text to display when the move is interrupted
|
||||
*/
|
||||
abstract interruptedText(pokemon: Pokemon, move: Moves): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag representing the "disabling" effect performed by {@linkcode Moves.DISABLE} and {@linkcode Abilities.CURSED_BODY}.
|
||||
* When the tag is added, the last-used move of the tag holder is set as the disabled move.
|
||||
*/
|
||||
export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
/** The move being disabled. Gets set when {@linkcode onAdd} is called for this tag. */
|
||||
private moveId: Moves = Moves.NONE;
|
||||
|
||||
constructor(sourceId: number) {
|
||||
super(BattlerTagType.DISABLED, 4, Moves.DISABLE, sourceId);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override isMoveRestricted(move: Moves): boolean {
|
||||
return move === this.moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@link moveId} and shows a message.
|
||||
* Otherwise the move ID will not get assigned and this tag will get removed next turn.
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
const move = pokemon.getLastXMoves()
|
||||
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||
if (move === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveId = move.move;
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:disabledOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override onRemove(pokemon: Pokemon): void {
|
||||
super.onRemove(pokemon);
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:disabledLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name });
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
|
||||
}
|
||||
|
||||
/** @override */
|
||||
override loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.moveId = source.moveId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
|
||||
*/
|
||||
@ -1995,6 +2116,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
return new StockpilingTag(sourceMove);
|
||||
case BattlerTagType.OCTOLOCK:
|
||||
return new OctolockTag(sourceId);
|
||||
case BattlerTagType.DISABLED:
|
||||
return new DisabledTag(sourceId);
|
||||
case BattlerTagType.IGNORE_GHOST:
|
||||
return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]);
|
||||
case BattlerTagType.IGNORE_DARK:
|
||||
|
@ -4332,72 +4332,6 @@ export class TypelessAttr extends MoveAttr { }
|
||||
*/
|
||||
export class BypassRedirectAttr extends MoveAttr { }
|
||||
|
||||
export class DisableMoveAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const moveQueue = target.getLastXMoves();
|
||||
let turnMove: TurnMove | undefined;
|
||||
while (moveQueue.length) {
|
||||
turnMove = moveQueue.shift();
|
||||
if (turnMove?.virtual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const moveIndex = target.getMoveset().findIndex(m => m?.moveId === turnMove?.move);
|
||||
if (moveIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const disabledMove = target.getMoveset()[moveIndex];
|
||||
target.summonData.disabledMove = disabledMove?.moveId!; // TODO: is this bang correct?
|
||||
target.summonData.disabledTurns = 4;
|
||||
|
||||
user.scene.queueMessage(i18next.t("abilityTriggers:postDefendMoveDisable", { pokemonNameWithAffix: getPokemonNameWithAffix(target), moveName: disabledMove?.getName()}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move): boolean => { // TODO: Not sure what to do here
|
||||
if (target.summonData.disabledMove || target.isMax()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const moveQueue = target.getLastXMoves();
|
||||
let turnMove: TurnMove | undefined;
|
||||
while (moveQueue.length) {
|
||||
turnMove = moveQueue.shift();
|
||||
if (turnMove?.virtual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const move = target.getMoveset().find(m => m?.moveId === turnMove?.move);
|
||||
if (!move) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
|
||||
return -5;
|
||||
}
|
||||
}
|
||||
|
||||
export class FrenzyAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT, false, true);
|
||||
@ -4488,6 +4422,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
case BattlerTagType.INFATUATED:
|
||||
case BattlerTagType.NIGHTMARE:
|
||||
case BattlerTagType.DROWSY:
|
||||
case BattlerTagType.DISABLED:
|
||||
return -5;
|
||||
case BattlerTagType.SEEDED:
|
||||
case BattlerTagType.SALT_CURED:
|
||||
@ -6673,7 +6608,8 @@ export function initMoves() {
|
||||
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
|
||||
.attr(FixedDamageAttr, 20),
|
||||
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
|
||||
.attr(DisableMoveAttr)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
|
||||
.condition(failOnMaxCondition),
|
||||
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||
|
@ -64,6 +64,7 @@ export enum BattlerTagType {
|
||||
STOCKPILING = "STOCKPILING",
|
||||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
||||
DISABLED = "DISABLED",
|
||||
IGNORE_GHOST = "IGNORE_GHOST",
|
||||
IGNORE_DARK = "IGNORE_DARK",
|
||||
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
||||
|
@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
|
||||
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
||||
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
|
||||
@ -2670,6 +2670,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the given move is currently disabled for this Pokemon.
|
||||
*
|
||||
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
|
||||
* @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false`
|
||||
*
|
||||
* @see {@linkcode MoveRestrictionBattlerTag}
|
||||
*/
|
||||
isMoveRestricted(moveId: Moves): boolean {
|
||||
return this.getRestrictingTag(moveId) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
|
||||
*
|
||||
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
|
||||
* @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
|
||||
*/
|
||||
getRestrictingTag(moveId: Moves): MoveRestrictionBattlerTag | null {
|
||||
for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) {
|
||||
if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) {
|
||||
return tag as MoveRestrictionBattlerTag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getMoveHistory(): TurnMove[] {
|
||||
return this.battleSummonData.moveHistory;
|
||||
}
|
||||
@ -4458,8 +4485,6 @@ export interface AttackMoveResult {
|
||||
export class PokemonSummonData {
|
||||
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||
public moveQueue: QueuedMove[] = [];
|
||||
public disabledMove: Moves = Moves.NONE;
|
||||
public disabledTurns: number = 0;
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed: boolean = false;
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
@ -4540,7 +4565,7 @@ export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | Hit
|
||||
* It links to {@linkcode Move} class via the move ID.
|
||||
* Compared to {@linkcode Move}, this class also tracks if a move has received.
|
||||
* PP Ups, amount of PP used, and things like that.
|
||||
* @see {@linkcode isUsable} - checks if move is disabled, out of PP, or not implemented.
|
||||
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
||||
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
||||
* @see {@linkcode usePp} - removes a point of PP from the move.
|
||||
* @see {@linkcode getMovePp} - returns amount of PP a move currently has.
|
||||
@ -4560,11 +4585,25 @@ export class PokemonMove {
|
||||
this.virtual = !!virtual;
|
||||
}
|
||||
|
||||
isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean {
|
||||
if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) {
|
||||
/**
|
||||
* Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets.
|
||||
* The move is unusable if it is out of PP, restricted by an effect, or unimplemented.
|
||||
*
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} that would be using this move
|
||||
* @param {boolean} ignorePp If `true`, skips the PP check
|
||||
* @param {boolean} ignoreRestrictionTags If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag})
|
||||
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
|
||||
*/
|
||||
isUsable(pokemon: Pokemon, ignorePp?: boolean, ignoreRestrictionTags?: boolean): boolean {
|
||||
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) {
|
||||
return false;
|
||||
}
|
||||
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)");
|
||||
|
||||
if (this.getMove().name.endsWith(" (N)")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1);
|
||||
}
|
||||
|
||||
getMove(): Move {
|
||||
|
@ -165,6 +165,7 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadImage("saving_icon", "ui");
|
||||
this.loadImage("discord", "ui");
|
||||
this.loadImage("google", "ui");
|
||||
this.loadImage("settings_icon", "ui");
|
||||
|
||||
this.loadImage("default_bg", "arenas");
|
||||
// Load arena images
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "Tatami-Schild",
|
||||
"craftyShield": "Trickschutz",
|
||||
"tailwind": "Rückenwind",
|
||||
"happyHour": "Goldene Zeiten"
|
||||
}
|
||||
"happyHour": "Goldene Zeiten",
|
||||
"safeguard": "Bodyguard"
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
|
||||
"moveNoPP": "There's no PP left for\nthis move!",
|
||||
"moveDisabled": "{{moveName}} is disabled!",
|
||||
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
|
||||
"noPokeballForce": "An unseen force\nprevents using Poké Balls.",
|
||||
"noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!",
|
||||
"noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!",
|
||||
|
@ -67,5 +67,7 @@
|
||||
"saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
}
|
||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
|
||||
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled."
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "Escudo Tatami",
|
||||
"craftyShield": "Truco Defensa",
|
||||
"tailwind": "Viento Afín",
|
||||
"happyHour": "Paga Extra"
|
||||
"happyHour": "Paga Extra",
|
||||
"safeguard": "Velo Sagrado"
|
||||
}
|
||||
|
@ -14,14 +14,14 @@
|
||||
"register": "Registrarse",
|
||||
"emptyUsername": "El usuario no puede estar vacío",
|
||||
"invalidLoginUsername": "El usuario no es válido",
|
||||
"invalidRegisterUsername": "El usuario solo puede contener letras, números y guiones bajos",
|
||||
"invalidRegisterUsername": "El usuario solo puede contener letras, números y guiones bajos.",
|
||||
"invalidLoginPassword": "La contraseña no es válida",
|
||||
"invalidRegisterPassword": "La contraseña debe tener 6 o más caracteres.",
|
||||
"usernameAlreadyUsed": "El usuario ya está en uso",
|
||||
"accountNonExistent": "El usuario no existe",
|
||||
"unmatchingPassword": "La contraseña no coincide",
|
||||
"passwordNotMatchingConfirmPassword": "Las contraseñas deben coincidir",
|
||||
"confirmPassword": "Confirmar Contra.",
|
||||
"confirmPassword": "Confirmar contraseña",
|
||||
"registrationAgeWarning": "Al registrarte, confirmas tener 13 o más años de edad.",
|
||||
"backToLogin": "Volver al Login",
|
||||
"failedToLoadSaveData": "No se han podido cargar los datos guardados. Por favor, recarga la página.\nSi el fallo continúa, por favor comprueba #announcements en nuestro Discord.",
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "Tatamigaeshi",
|
||||
"craftyShield": "Vigilance",
|
||||
"tailwind": "Vent Arrière",
|
||||
"happyHour": "Étrennes"
|
||||
}
|
||||
"happyHour": "Étrennes",
|
||||
"safeguard": "Rune Protect"
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "Ribaltappeto",
|
||||
"craftyShield": "Truccodifesa",
|
||||
"tailwind": "Ventoincoda",
|
||||
"happyHour": "Cuccagna"
|
||||
}
|
||||
"happyHour": "Cuccagna",
|
||||
"safeguard": "Salvaguardia"
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "마룻바닥세워막기",
|
||||
"craftyShield": "트릭가드",
|
||||
"tailwind": "순풍",
|
||||
"happyHour": "해피타임"
|
||||
}
|
||||
"happyHour": "해피타임",
|
||||
"safeguard": "신비의부적"
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "Mat Block",
|
||||
"craftyShield": "Crafty Shield",
|
||||
"tailwind": "Tailwind",
|
||||
"happyHour": "Happy Hour"
|
||||
"happyHour": "Happy Hour",
|
||||
"safeguard": "Safeguard"
|
||||
}
|
||||
|
@ -36,5 +36,6 @@
|
||||
"matBlock": "掀榻榻米",
|
||||
"craftyShield": "戏法防守",
|
||||
"tailwind": "顺风",
|
||||
"happyHour": "快乐时光"
|
||||
}
|
||||
"happyHour": "快乐时光",
|
||||
"safeguard": "神秘守护"
|
||||
}
|
||||
|
@ -1,31 +1,34 @@
|
||||
import BattleScene from "#app/battle-scene.js";
|
||||
import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability.js";
|
||||
import { Stat } from "#app/enums/stat.js";
|
||||
import { StatusEffect } from "#app/enums/status-effect.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { StatusEffect } from "#app/enums/status-effect";
|
||||
import Pokemon, { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
|
||||
import i18next from "i18next";
|
||||
import * as Utils from "#app/utils.js";
|
||||
import * as Utils from "#app/utils";
|
||||
import { BattleEndPhase } from "./battle-end-phase";
|
||||
import { NewBattlePhase } from "./new-battle-phase";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
|
||||
export class AttemptRunPhase extends PokemonPhase {
|
||||
constructor(scene: BattleScene, fieldIndex: integer) {
|
||||
constructor(scene: BattleScene, fieldIndex: number) {
|
||||
super(scene, fieldIndex);
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
const playerPokemon = this.getPokemon();
|
||||
const playerField = this.scene.getPlayerField();
|
||||
const enemyField = this.scene.getEnemyField();
|
||||
|
||||
const enemySpeed = enemyField.reduce((total: integer, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD), 0) / enemyField.length;
|
||||
const playerPokemon = this.getPokemon();
|
||||
|
||||
const escapeChance = new Utils.NumberHolder(0);
|
||||
|
||||
this.attemptRunAway(playerField, enemyField, escapeChance);
|
||||
|
||||
const escapeChance = new Utils.IntegerHolder((((playerPokemon.getStat(Stat.SPD) * 128) / enemySpeed) + (30 * this.scene.currentBattle.escapeAttempts++)) % 256);
|
||||
applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance);
|
||||
|
||||
if (playerPokemon.randSeedInt(256) < escapeChance.value) {
|
||||
if (Utils.randSeedInt(100) < escapeChance.value) {
|
||||
this.scene.playSound("se/flee");
|
||||
this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
|
||||
|
||||
@ -53,4 +56,48 @@ export class AttemptRunPhase extends PokemonPhase {
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
attemptRunAway(playerField: PlayerPokemon[], enemyField: EnemyPokemon[], escapeChance: Utils.NumberHolder) {
|
||||
/** Sum of the speed of all enemy pokemon on the field */
|
||||
const enemySpeed = enemyField.reduce((total: number, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD), 0);
|
||||
/** Sum of the speed of all player pokemon on the field */
|
||||
const playerSpeed = playerField.reduce((total: number, playerPokemon: Pokemon) => total + playerPokemon.getStat(Stat.SPD), 0);
|
||||
|
||||
/* The way the escape chance works is by looking at the difference between your speed and the enemy field's average speed as a ratio. The higher this ratio, the higher your chance of success.
|
||||
* However, there is a cap for the ratio of your speed vs enemy speed which beyond that point, you won't gain any advantage. It also looks at how many times you've tried to escape.
|
||||
* Again, the more times you've tried to escape, the higher your odds of escaping. Bosses and non-bosses are calculated differently - bosses are harder to escape from vs non-bosses
|
||||
* Finally, there's a minimum and maximum escape chance as well so that escapes aren't guaranteed, yet they are never 0 either.
|
||||
* The percentage chance to escape from a pokemon for both bosses and non bosses is linear and based on the minimum and maximum chances, and the speed ratio cap.
|
||||
*
|
||||
* At the time of writing, these conditions should be met:
|
||||
* - The minimum escape chance should be 5% for bosses and non bosses
|
||||
* - Bosses should have a maximum escape chance of 25%, whereas non-bosses should be 95%
|
||||
* - The bonus per previous escape attempt should be 2% for bosses and 10% for non-bosses
|
||||
* - The speed ratio cap should be 6x for bosses and 4x for non-bosses
|
||||
* - The "default" escape chance when your speed equals the enemy speed should be 8.33% for bosses and 27.5% for non-bosses
|
||||
*
|
||||
* From the above, we can calculate the below values
|
||||
*/
|
||||
|
||||
let isBoss = false;
|
||||
for (let e = 0; e < enemyField.length; e++) {
|
||||
isBoss = isBoss || enemyField[e].isBoss(); // this line checks if any of the enemy pokemon on the field are bosses; if so, the calculation for escaping is different
|
||||
}
|
||||
|
||||
/** The ratio between the speed of your active pokemon and the speed of the enemy field */
|
||||
const speedRatio = playerSpeed / enemySpeed;
|
||||
/** The max ratio before escape chance stops increasing. Increased if there is a boss on the field */
|
||||
const speedCap = isBoss ? 6 : 4;
|
||||
/** Minimum percent chance to escape */
|
||||
const minChance = 5;
|
||||
/** Maximum percent chance to escape. Decreased if a boss is on the field */
|
||||
const maxChance = isBoss ? 25 : 95;
|
||||
/** How much each escape attempt increases the chance of the next attempt. Decreased if a boss is on the field */
|
||||
const escapeBonus = isBoss ? 2 : 10;
|
||||
/** Slope of the escape chance curve */
|
||||
const escapeSlope = (maxChance - minChance) / speedCap;
|
||||
|
||||
// This will calculate the escape chance given all of the above and clamp it to the range of [`minChance`, `maxChance`]
|
||||
escapeChance.value = Phaser.Math.Clamp(Math.round((escapeSlope * speedRatio) + minChance + (escapeBonus * this.scene.currentBattle.escapeAttempts++)), minChance, maxChance);
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +107,9 @@ export class CommandPhase extends FieldPhase {
|
||||
|
||||
// Decides between a Disabled, Not Implemented, or No PP translation message
|
||||
const errorMessage =
|
||||
playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" :
|
||||
move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
|
||||
playerPokemon.isMoveRestricted(move.moveId)
|
||||
? playerPokemon.getRestrictingTag(move.moveId)!.selectionDeniedText(playerPokemon, move.moveId)
|
||||
: move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
|
||||
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
||||
|
||||
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
|
||||
|
@ -44,8 +44,8 @@ export class MovePhase extends BattlePhase {
|
||||
this.cancelled = false;
|
||||
}
|
||||
|
||||
canMove(): boolean {
|
||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length;
|
||||
canMove(ignoreDisableTags?: boolean): boolean {
|
||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) && !!this.targets.length;
|
||||
}
|
||||
|
||||
/**Signifies the current move should fail but still use PP */
|
||||
@ -63,10 +63,7 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
console.log(Moves[this.move.moveId]);
|
||||
|
||||
if (!this.canMove()) {
|
||||
if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) {
|
||||
this.scene.queueMessage(i18next.t("battle:moveDisabled", { moveName: this.move.getName() }));
|
||||
}
|
||||
if (!this.canMove(true)) {
|
||||
if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails
|
||||
this.fail();
|
||||
this.showMoveText();
|
||||
|
@ -1,9 +1,7 @@
|
||||
import BattleScene from "#app/battle-scene.js";
|
||||
import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/ability.js";
|
||||
import { BattlerTagLapseType } from "#app/data/battler-tags.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { TerrainType } from "#app/data/terrain.js";
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { WeatherType } from "#app/enums/weather-type.js";
|
||||
import { TurnEndEvent } from "#app/events/battle-scene.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
@ -11,7 +9,6 @@ import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||
import { TurnHealModifier, EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier, TurnStatusEffectModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
|
||||
import i18next from "i18next";
|
||||
import { FieldPhase } from "./field-phase";
|
||||
import { MessagePhase } from "./message-phase";
|
||||
import { PokemonHealPhase } from "./pokemon-heal-phase";
|
||||
|
||||
export class TurnEndPhase extends FieldPhase {
|
||||
@ -28,11 +25,6 @@ export class TurnEndPhase extends FieldPhase {
|
||||
const handlePokemon = (pokemon: Pokemon) => {
|
||||
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
|
||||
|
||||
if (pokemon.summonData.disabledMove && !--pokemon.summonData.disabledTurns) {
|
||||
this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name })));
|
||||
pokemon.summonData.disabledMove = Moves.NONE;
|
||||
}
|
||||
|
||||
this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon);
|
||||
|
||||
if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) {
|
||||
|
@ -127,8 +127,6 @@ export default class PokemonData {
|
||||
this.summonData.stats = source.summonData.stats;
|
||||
this.summonData.statStages = source.summonData.statStages;
|
||||
this.summonData.moveQueue = source.summonData.moveQueue;
|
||||
this.summonData.disabledMove = source.summonData.disabledMove;
|
||||
this.summonData.disabledTurns = source.summonData.disabledTurns;
|
||||
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
||||
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
|
||||
|
||||
|
@ -2,12 +2,6 @@ import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
import { Type } from "#app/data/type";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { DamagePhase } from "#app/phases/damage-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { toDmgValue } from "#app/utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -15,7 +9,7 @@ import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
@ -39,36 +33,31 @@ describe("Abilities - Parental Bond", () => {
|
||||
game.override.disableCrits();
|
||||
game.override.ability(Abilities.PARENTAL_BOND);
|
||||
game.override.enemySpecies(Species.SNORLAX);
|
||||
game.override.enemyAbility(Abilities.INSOMNIA);
|
||||
game.override.enemyAbility(Abilities.FUR_COAT);
|
||||
game.override.enemyMoveset(SPLASH_ONLY);
|
||||
game.override.startingLevel(100);
|
||||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"ability should add second strike to attack move",
|
||||
it(
|
||||
"should add second strike to attack move",
|
||||
async () => {
|
||||
game.override.moveset([Moves.TACKLE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
let enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
const firstStrikeDamage = enemyStartingHp - enemyPokemon.hp;
|
||||
enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
const secondStrikeDamage = enemyStartingHp - enemyPokemon.hp;
|
||||
|
||||
@ -77,556 +66,460 @@ describe("Abilities - Parental Bond", () => {
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should apply secondary effects to both strikes",
|
||||
it(
|
||||
"should apply secondary effects to both strikes",
|
||||
async () => {
|
||||
game.override.moveset([Moves.POWER_UP_PUNCH]);
|
||||
game.override.enemySpecies(Species.AMOONGUSS);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.POWER_UP_PUNCH);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to Status moves",
|
||||
it(
|
||||
"should not apply to Status moves",
|
||||
async () => {
|
||||
game.override.moveset([Moves.BABY_DOLL_EYES]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.BABY_DOLL_EYES);
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to multi-hit moves",
|
||||
it(
|
||||
"should not apply to multi-hit moves",
|
||||
async () => {
|
||||
game.override.moveset([Moves.DOUBLE_HIT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.DOUBLE_HIT);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to self-sacrifice moves",
|
||||
it(
|
||||
"should not apply to self-sacrifice moves",
|
||||
async () => {
|
||||
game.override.moveset([Moves.SELF_DESTRUCT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.SELF_DESTRUCT);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
await game.phaseInterceptor.to("DamagePhase", false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to Rollout",
|
||||
it(
|
||||
"should not apply to Rollout",
|
||||
async () => {
|
||||
game.override.moveset([Moves.ROLLOUT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.ROLLOUT);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
await game.phaseInterceptor.to("DamagePhase", false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply multiplier to fixed-damage moves",
|
||||
it(
|
||||
"should not apply multiplier to fixed-damage moves",
|
||||
async () => {
|
||||
game.override.moveset([Moves.DRAGON_RAGE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(Moves.DRAGON_RAGE);
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp - 80);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - 80);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply multiplier to counter moves",
|
||||
it(
|
||||
"should not apply multiplier to counter moves",
|
||||
async () => {
|
||||
game.override.moveset([Moves.COUNTER]);
|
||||
game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.SHUCKLE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const playerStartingHp = leadPokemon.hp;
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(Moves.COUNTER);
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
const playerDamage = playerStartingHp - leadPokemon.hp;
|
||||
const playerDamage = leadPokemon.getMaxHp() - leadPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp - 4 * playerDamage);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - 4 * playerDamage);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to multi-target moves",
|
||||
it(
|
||||
"should not apply to multi-target moves",
|
||||
async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.moveset([Moves.EARTHQUAKE]);
|
||||
game.override.passiveAbility(Abilities.LEVITATE);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.PIDGEOT]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
expect(playerPokemon.length).toBe(2);
|
||||
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
expect(enemyPokemon.length).toBe(2);
|
||||
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
game.move.select(Moves.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.move.select(Moves.EARTHQUAKE, 1);
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
playerPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should apply to multi-target moves when hitting only one target",
|
||||
it(
|
||||
"should apply to multi-target moves when hitting only one target",
|
||||
async () => {
|
||||
game.override.moveset([Moves.EARTHQUAKE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
await game.phaseInterceptor.to("DamagePhase", false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should only trigger post-target move effects once",
|
||||
it(
|
||||
"should only trigger post-target move effects once",
|
||||
async () => {
|
||||
game.override.moveset([Moves.MIND_BLOWN]);
|
||||
|
||||
await game.startBattle([Species.PIDGEOT]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.MIND_BLOWN);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
await game.phaseInterceptor.to("DamagePhase", false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
// This test will time out if the user faints
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(toDmgValue(leadPokemon.getMaxHp() / 2));
|
||||
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() / 2));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Burn Up only removes type after second strike with this ability",
|
||||
it(
|
||||
"Burn Up only removes type after the second strike",
|
||||
async () => {
|
||||
game.override.moveset([Moves.BURN_UP]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.BURN_UP);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.hp).toBeGreaterThan(0);
|
||||
expect(leadPokemon.isOfType(Type.FIRE)).toBe(true);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.isOfType(Type.FIRE)).toBe(false);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"Moves boosted by this ability and Multi-Lens should strike 4 times",
|
||||
async () => {
|
||||
game.override.moveset([Moves.TACKLE]);
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(4);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"Super Fang boosted by this ability and Multi-Lens should strike twice",
|
||||
async () => {
|
||||
game.override.moveset([Moves.SUPER_FANG]);
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(Moves.SUPER_FANG);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase, false);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(Math.ceil(enemyStartingHp * 0.25));
|
||||
expect(enemyPokemon.hp).toBe(Math.ceil(enemyPokemon.getMaxHp() * 0.25));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"Seismic Toss boosted by this ability and Multi-Lens should strike twice",
|
||||
async () => {
|
||||
game.override.moveset([Moves.SEISMIC_TOSS]);
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.move.select(Moves.SEISMIC_TOSS);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase, false);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp - 200);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"Hyper Beam boosted by this ability should strike twice, then recharge",
|
||||
async () => {
|
||||
game.override.moveset([Moves.HYPER_BEAM]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.HYPER_BEAM);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
/** TODO: Fix TRAPPED tag lapsing incorrectly, then run this test */
|
||||
test(
|
||||
it(
|
||||
"Anchor Shot boosted by this ability should only trap the target after the second hit",
|
||||
async () => {
|
||||
game.override.moveset([Moves.ANCHOR_SHOT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.ANCHOR_SHOT);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); // Passes
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Passes
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Fails :(
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"Smack Down boosted by this ability should only ground the target after the second hit",
|
||||
async () => {
|
||||
game.override.moveset([Moves.SMACK_DOWN]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.SMACK_DOWN);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"U-turn boosted by this ability should strike twice before forcing a switch",
|
||||
async () => {
|
||||
game.override.moveset([Moves.U_TURN]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.U_TURN);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
// This will cause this test to time out if the switch was forced on the first hit.
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
it(
|
||||
"Wake-Up Slap boosted by this ability should only wake up the target after the second hit",
|
||||
async () => {
|
||||
game.override.moveset([Moves.WAKE_UP_SLAP]).enemyStatusEffect(StatusEffect.SLEEP);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.WAKE_UP_SLAP);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.status?.effect).toBeUndefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not cause user to hit into King's Shield more than once",
|
||||
it(
|
||||
"should not cause user to hit into King's Shield more than once",
|
||||
async () => {
|
||||
game.override.moveset([Moves.TACKLE]);
|
||||
game.override.enemyMoveset([Moves.KINGS_SHIELD, Moves.KINGS_SHIELD, Moves.KINGS_SHIELD, Moves.KINGS_SHIELD]);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.KINGS_SHIELD));
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not cause user to hit into Storm Drain more than once",
|
||||
it(
|
||||
"should not cause user to hit into Storm Drain more than once",
|
||||
async () => {
|
||||
game.override.moveset([Moves.WATER_GUN]);
|
||||
game.override.enemyAbility(Abilities.STORM_DRAIN);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.move.select(Moves.WATER_GUN);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to multi-target moves with Multi-Lens",
|
||||
it(
|
||||
"should not apply to multi-target moves with Multi-Lens",
|
||||
async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.moveset([Moves.EARTHQUAKE, Moves.SPLASH]);
|
||||
game.override.passiveAbility(Abilities.LEVITATE);
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.PIDGEOT]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
expect(playerPokemon.length).toBe(2);
|
||||
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
expect(enemyPokemon.length).toBe(2);
|
||||
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
const enemyStartingHp = enemyPokemon.map(p => p.hp);
|
||||
|
||||
game.move.select(Moves.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
await game.phaseInterceptor.to("DamagePhase");
|
||||
const enemyFirstHitDamage = enemyStartingHp.map((hp, i) => hp - enemyPokemon[i].hp);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2 * enemyFirstHitDamage[i]));
|
||||
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
||||
|
303
src/test/escape-calculations.test.ts
Normal file
303
src/test/escape-calculations.test.ts
Normal file
@ -0,0 +1,303 @@
|
||||
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { Command } from "#app/ui/command-ui-handler";
|
||||
import * as Utils from "#app/utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Escape chance calculations", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleType("single")
|
||||
.enemySpecies(Species.BULBASAUR)
|
||||
.enemyAbility(Abilities.INSOMNIA)
|
||||
.ability(Abilities.INSOMNIA);
|
||||
});
|
||||
|
||||
it("single non-boss opponent", async () => {
|
||||
await game.classicMode.startBattle([Species.BULBASAUR]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
const enemyField = game.scene.getEnemyField();
|
||||
const enemySpeed = 100;
|
||||
// set enemyPokemon's speed to 100
|
||||
vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]);
|
||||
|
||||
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new Utils.NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: { pokemonSpeedRatio: number, escapeAttempts: number, expectedEscapeChance: number }[] = [
|
||||
{ pokemonSpeedRatio: 0.01, escapeAttempts: 0, expectedEscapeChance: 5 },
|
||||
{ pokemonSpeedRatio: 0.1, escapeAttempts: 0, expectedEscapeChance: 7 },
|
||||
{ pokemonSpeedRatio: 0.25, escapeAttempts: 0, expectedEscapeChance: 11 },
|
||||
{ pokemonSpeedRatio: 0.5, escapeAttempts: 0, expectedEscapeChance: 16 },
|
||||
{ pokemonSpeedRatio: 0.8, escapeAttempts: 0, expectedEscapeChance: 23 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 0, expectedEscapeChance: 28 },
|
||||
{ pokemonSpeedRatio: 1.2, escapeAttempts: 0, expectedEscapeChance: 32 },
|
||||
{ pokemonSpeedRatio: 1.5, escapeAttempts: 0, expectedEscapeChance: 39 },
|
||||
{ pokemonSpeedRatio: 3, escapeAttempts: 0, expectedEscapeChance: 73 },
|
||||
{ pokemonSpeedRatio: 3.8, escapeAttempts: 0, expectedEscapeChance: 91 },
|
||||
{ pokemonSpeedRatio: 4, escapeAttempts: 0, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 4.2, escapeAttempts: 0, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 10, escapeAttempts: 0, expectedEscapeChance: 95 },
|
||||
|
||||
// retries section
|
||||
{ pokemonSpeedRatio: 0.4, escapeAttempts: 1, expectedEscapeChance: 24 },
|
||||
{ pokemonSpeedRatio: 1.6, escapeAttempts: 2, expectedEscapeChance: 61 },
|
||||
{ pokemonSpeedRatio: 3.7, escapeAttempts: 5, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 0.2, escapeAttempts: 2, expectedEscapeChance: 30 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 3, expectedEscapeChance: 58 },
|
||||
{ pokemonSpeedRatio: 2.9, escapeAttempts: 0, expectedEscapeChance: 70 },
|
||||
{ pokemonSpeedRatio: 0.01, escapeAttempts: 7, expectedEscapeChance: 75 },
|
||||
{ pokemonSpeedRatio: 16.2, escapeAttempts: 4, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 2, escapeAttempts: 3, expectedEscapeChance: 80 },
|
||||
];
|
||||
|
||||
for (let i = 0; i < escapeChances.length; i++) {
|
||||
// sets the number of escape attempts to the required amount
|
||||
game.scene.currentBattle.escapeAttempts = escapeChances[i].escapeAttempts;
|
||||
// set playerPokemon's speed to a multiple of the enemySpeed
|
||||
vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, escapeChances[i].pokemonSpeedRatio * enemySpeed]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
it("double non-boss opponent", async () => {
|
||||
game.override.battleType("double");
|
||||
await game.classicMode.startBattle([Species.BULBASAUR, Species.ABOMASNOW]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
const enemyField = game.scene.getEnemyField();
|
||||
const enemyASpeed = 70;
|
||||
const enemyBSpeed = 30;
|
||||
// gets the sum of the speed of the two pokemon
|
||||
const totalEnemySpeed = enemyASpeed + enemyBSpeed;
|
||||
// this is used to find the ratio of the player's first pokemon
|
||||
const playerASpeedPercentage = 0.4;
|
||||
// set enemyAPokemon's speed to 70
|
||||
vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyASpeed]);
|
||||
// set enemyBPokemon's speed to 30
|
||||
vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]);
|
||||
|
||||
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new Utils.NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: { pokemonSpeedRatio: number, escapeAttempts: number, expectedEscapeChance: number }[] = [
|
||||
{ pokemonSpeedRatio: 0.3, escapeAttempts: 0, expectedEscapeChance: 12 },
|
||||
{ pokemonSpeedRatio: 0.7, escapeAttempts: 0, expectedEscapeChance: 21 },
|
||||
{ pokemonSpeedRatio: 1.5, escapeAttempts: 0, expectedEscapeChance: 39 },
|
||||
{ pokemonSpeedRatio: 3, escapeAttempts: 0, expectedEscapeChance: 73 },
|
||||
{ pokemonSpeedRatio: 9, escapeAttempts: 0, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 0.01, escapeAttempts: 0, expectedEscapeChance: 5 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 0, expectedEscapeChance: 28 },
|
||||
{ pokemonSpeedRatio: 4.3, escapeAttempts: 0, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 2.7, escapeAttempts: 0, expectedEscapeChance: 66 },
|
||||
{ pokemonSpeedRatio: 2.1, escapeAttempts: 0, expectedEscapeChance: 52 },
|
||||
{ pokemonSpeedRatio: 1.8, escapeAttempts: 0, expectedEscapeChance: 46 },
|
||||
{ pokemonSpeedRatio: 6, escapeAttempts: 0, expectedEscapeChance: 95 },
|
||||
|
||||
// retries section
|
||||
{ pokemonSpeedRatio: 0.9, escapeAttempts: 1, expectedEscapeChance: 35 },
|
||||
{ pokemonSpeedRatio: 3.6, escapeAttempts: 2, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 0.03, escapeAttempts: 7, expectedEscapeChance: 76 },
|
||||
{ pokemonSpeedRatio: 0.02, escapeAttempts: 7, expectedEscapeChance: 75 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 5, expectedEscapeChance: 78 },
|
||||
{ pokemonSpeedRatio: 0.7, escapeAttempts: 3, expectedEscapeChance: 51 },
|
||||
{ pokemonSpeedRatio: 2.4, escapeAttempts: 9, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 1.8, escapeAttempts: 7, expectedEscapeChance: 95 },
|
||||
{ pokemonSpeedRatio: 2, escapeAttempts: 10, expectedEscapeChance: 95 },
|
||||
|
||||
];
|
||||
|
||||
for (let i = 0; i < escapeChances.length; i++) {
|
||||
// sets the number of escape attempts to the required amount
|
||||
game.scene.currentBattle.escapeAttempts = escapeChances[i].escapeAttempts;
|
||||
// set the first playerPokemon's speed to a multiple of the enemySpeed
|
||||
vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, Math.floor(escapeChances[i].pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage)]);
|
||||
// set the second playerPokemon's speed to the remaining value of speed
|
||||
vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5]]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
// checks to make sure the escape values are the same
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
// checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed
|
||||
expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe(escapeChances[i].pokemonSpeedRatio * totalEnemySpeed);
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
it("single boss opponent", async () => {
|
||||
game.override.startingWave(10);
|
||||
await game.classicMode.startBattle([Species.BULBASAUR]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField()!;
|
||||
const enemyField = game.scene.getEnemyField()!;
|
||||
const enemySpeed = 100;
|
||||
// set enemyPokemon's speed to 100
|
||||
vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]);
|
||||
|
||||
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new Utils.NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: { pokemonSpeedRatio: number, escapeAttempts: number, expectedEscapeChance: number }[] = [
|
||||
{ pokemonSpeedRatio: 0.01, escapeAttempts: 0, expectedEscapeChance: 5 },
|
||||
{ pokemonSpeedRatio: 0.1, escapeAttempts: 0, expectedEscapeChance: 5 },
|
||||
{ pokemonSpeedRatio: 0.25, escapeAttempts: 0, expectedEscapeChance: 6 },
|
||||
{ pokemonSpeedRatio: 0.5, escapeAttempts: 0, expectedEscapeChance: 7 },
|
||||
{ pokemonSpeedRatio: 0.8, escapeAttempts: 0, expectedEscapeChance: 8 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 0, expectedEscapeChance: 8 },
|
||||
{ pokemonSpeedRatio: 1.2, escapeAttempts: 0, expectedEscapeChance: 9 },
|
||||
{ pokemonSpeedRatio: 1.5, escapeAttempts: 0, expectedEscapeChance: 10 },
|
||||
{ pokemonSpeedRatio: 3, escapeAttempts: 0, expectedEscapeChance: 15 },
|
||||
{ pokemonSpeedRatio: 3.8, escapeAttempts: 0, expectedEscapeChance: 18 },
|
||||
{ pokemonSpeedRatio: 4, escapeAttempts: 0, expectedEscapeChance: 18 },
|
||||
{ pokemonSpeedRatio: 4.2, escapeAttempts: 0, expectedEscapeChance: 19 },
|
||||
{ pokemonSpeedRatio: 4.7, escapeAttempts: 0, expectedEscapeChance: 21 },
|
||||
{ pokemonSpeedRatio: 5, escapeAttempts: 0, expectedEscapeChance: 22 },
|
||||
{ pokemonSpeedRatio: 5.9, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 6, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 6.7, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 10, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
|
||||
// retries section
|
||||
{ pokemonSpeedRatio: 0.4, escapeAttempts: 1, expectedEscapeChance: 8 },
|
||||
{ pokemonSpeedRatio: 1.6, escapeAttempts: 2, expectedEscapeChance: 14 },
|
||||
{ pokemonSpeedRatio: 3.7, escapeAttempts: 5, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 0.2, escapeAttempts: 2, expectedEscapeChance: 10 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 3, expectedEscapeChance: 14 },
|
||||
{ pokemonSpeedRatio: 2.9, escapeAttempts: 0, expectedEscapeChance: 15 },
|
||||
{ pokemonSpeedRatio: 0.01, escapeAttempts: 7, expectedEscapeChance: 19 },
|
||||
{ pokemonSpeedRatio: 16.2, escapeAttempts: 4, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 2, escapeAttempts: 3, expectedEscapeChance: 18 },
|
||||
{ pokemonSpeedRatio: 4.5, escapeAttempts: 1, expectedEscapeChance: 22 },
|
||||
{ pokemonSpeedRatio: 6.8, escapeAttempts: 6, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 5.2, escapeAttempts: 8, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 4.7, escapeAttempts: 10, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 5.1, escapeAttempts: 1, expectedEscapeChance: 24 },
|
||||
{ pokemonSpeedRatio: 6, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 5.9, escapeAttempts: 2, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 6.1, escapeAttempts: 3, expectedEscapeChance: 25 },
|
||||
|
||||
];
|
||||
|
||||
for (let i = 0; i < escapeChances.length; i++) {
|
||||
// sets the number of escape attempts to the required amount
|
||||
game.scene.currentBattle.escapeAttempts = escapeChances[i].escapeAttempts;
|
||||
// set playerPokemon's speed to a multiple of the enemySpeed
|
||||
vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, escapeChances[i].pokemonSpeedRatio * enemySpeed]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
it("double boss opponent", async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.startingWave(10);
|
||||
await game.classicMode.startBattle([Species.BULBASAUR, Species.ABOMASNOW]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
const enemyField = game.scene.getEnemyField();
|
||||
const enemyASpeed = 70;
|
||||
const enemyBSpeed = 30;
|
||||
// gets the sum of the speed of the two pokemon
|
||||
const totalEnemySpeed = enemyASpeed + enemyBSpeed;
|
||||
// this is used to find the ratio of the player's first pokemon
|
||||
const playerASpeedPercentage = 0.8;
|
||||
// set enemyAPokemon's speed to 70
|
||||
vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyASpeed]);
|
||||
// set enemyBPokemon's speed to 30
|
||||
vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]);
|
||||
|
||||
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new Utils.NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: { pokemonSpeedRatio: number, escapeAttempts: number, expectedEscapeChance: number }[] = [
|
||||
{ pokemonSpeedRatio: 0.3, escapeAttempts: 0, expectedEscapeChance: 6 },
|
||||
{ pokemonSpeedRatio: 0.7, escapeAttempts: 0, expectedEscapeChance: 7 },
|
||||
{ pokemonSpeedRatio: 1.5, escapeAttempts: 0, expectedEscapeChance: 10 },
|
||||
{ pokemonSpeedRatio: 3, escapeAttempts: 0, expectedEscapeChance: 15 },
|
||||
{ pokemonSpeedRatio: 9, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 0.01, escapeAttempts: 0, expectedEscapeChance: 5 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 0, expectedEscapeChance: 8 },
|
||||
{ pokemonSpeedRatio: 4.3, escapeAttempts: 0, expectedEscapeChance: 19 },
|
||||
{ pokemonSpeedRatio: 2.7, escapeAttempts: 0, expectedEscapeChance: 14 },
|
||||
{ pokemonSpeedRatio: 2.1, escapeAttempts: 0, expectedEscapeChance: 12 },
|
||||
{ pokemonSpeedRatio: 1.8, escapeAttempts: 0, expectedEscapeChance: 11 },
|
||||
{ pokemonSpeedRatio: 6, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 4, escapeAttempts: 0, expectedEscapeChance: 18 },
|
||||
{ pokemonSpeedRatio: 5.7, escapeAttempts: 0, expectedEscapeChance: 24 },
|
||||
{ pokemonSpeedRatio: 5, escapeAttempts: 0, expectedEscapeChance: 22 },
|
||||
{ pokemonSpeedRatio: 6.1, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 6.8, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 10, escapeAttempts: 0, expectedEscapeChance: 25 },
|
||||
|
||||
// retries section
|
||||
{ pokemonSpeedRatio: 0.9, escapeAttempts: 1, expectedEscapeChance: 10 },
|
||||
{ pokemonSpeedRatio: 3.6, escapeAttempts: 2, expectedEscapeChance: 21 },
|
||||
{ pokemonSpeedRatio: 0.03, escapeAttempts: 7, expectedEscapeChance: 19 },
|
||||
{ pokemonSpeedRatio: 0.02, escapeAttempts: 7, expectedEscapeChance: 19 },
|
||||
{ pokemonSpeedRatio: 1, escapeAttempts: 5, expectedEscapeChance: 18 },
|
||||
{ pokemonSpeedRatio: 0.7, escapeAttempts: 3, expectedEscapeChance: 13 },
|
||||
{ pokemonSpeedRatio: 2.4, escapeAttempts: 9, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 1.8, escapeAttempts: 7, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 2, escapeAttempts: 10, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 3, escapeAttempts: 1, expectedEscapeChance: 17 },
|
||||
{ pokemonSpeedRatio: 4.5, escapeAttempts: 3, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 3.7, escapeAttempts: 1, expectedEscapeChance: 19 },
|
||||
{ pokemonSpeedRatio: 6.5, escapeAttempts: 1, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 12, escapeAttempts: 4, expectedEscapeChance: 25 },
|
||||
{ pokemonSpeedRatio: 5.2, escapeAttempts: 2, expectedEscapeChance: 25 },
|
||||
|
||||
];
|
||||
|
||||
for (let i = 0; i < escapeChances.length; i++) {
|
||||
// sets the number of escape attempts to the required amount
|
||||
game.scene.currentBattle.escapeAttempts = escapeChances[i].escapeAttempts;
|
||||
// set the first playerPokemon's speed to a multiple of the enemySpeed
|
||||
vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, Math.floor(escapeChances[i].pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage)]);
|
||||
// set the second playerPokemon's speed to the remaining value of speed
|
||||
vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5]]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
// checks to make sure the escape values are the same
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
// checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed
|
||||
expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe(escapeChances[i].pokemonSpeedRatio * totalEnemySpeed);
|
||||
}
|
||||
}, 20000);
|
||||
});
|
129
src/test/moves/disable.test.ts
Normal file
129
src/test/moves/disable.test.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Disable", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleType("single")
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.DISABLE, Moves.SPLASH])
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.starterSpecies(Species.PIKACHU)
|
||||
.enemySpecies(Species.SHUCKLE);
|
||||
});
|
||||
|
||||
it("restricts moves", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyMon.getMoveHistory()).toHaveLength(1);
|
||||
expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(true);
|
||||
});
|
||||
|
||||
it("fails if enemy has no move history", async() => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerMon = game.scene.getPlayerPokemon()!;
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(playerMon.getMoveHistory()[0]).toMatchObject({ move: Moves.DISABLE, result: MoveResult.FAIL });
|
||||
expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(false);
|
||||
}, 20000);
|
||||
|
||||
it("causes STRUGGLE if all usable moves are disabled", async() => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
const enemyHistory = enemyMon.getMoveHistory();
|
||||
expect(enemyHistory).toHaveLength(2);
|
||||
expect(enemyHistory[0].move).toBe(Moves.SPLASH);
|
||||
expect(enemyHistory[1].move).toBe(Moves.STRUGGLE);
|
||||
}, 20000);
|
||||
|
||||
it("cannot disable STRUGGLE", async() => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.STRUGGLE));
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerMon = game.scene.getPlayerPokemon()!;
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(playerMon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(enemyMon.getLastXMoves()[0].move).toBe(Moves.STRUGGLE);
|
||||
expect(enemyMon.isMoveRestricted(Moves.STRUGGLE)).toBe(false);
|
||||
}, 20000);
|
||||
|
||||
it("interrupts target's move when target moves after", async() => {
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Both mons just used Splash last turn; now have player use Disable.
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
const enemyHistory = enemyMon.getMoveHistory();
|
||||
expect(enemyHistory).toHaveLength(2);
|
||||
expect(enemyHistory[0]).toMatchObject({ move: Moves.SPLASH, result: MoveResult.SUCCESS });
|
||||
expect(enemyHistory[1].result).toBe(MoveResult.FAIL);
|
||||
}, 20000);
|
||||
|
||||
it("disables NATURE POWER, not the move invoked by it", async() => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.NATURE_POWER));
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyMon.isMoveRestricted(Moves.NATURE_POWER)).toBe(true);
|
||||
expect(enemyMon.isMoveRestricted(enemyMon.getLastXMoves(2)[1].move)).toBe(false);
|
||||
}, 20000);
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { Phase } from "#app/phase";
|
||||
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
|
||||
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
||||
@ -100,6 +101,7 @@ export default class PhaseInterceptor {
|
||||
[EvolutionPhase, this.startPhase],
|
||||
[EndEvolutionPhase, this.startPhase],
|
||||
[LevelCapPhase, this.startPhase],
|
||||
[AttemptRunPhase, this.startPhase],
|
||||
];
|
||||
|
||||
private endBySetMode = [
|
||||
|
@ -3,14 +3,17 @@ import { ModalConfig } from "./modal-ui-handler";
|
||||
import * as Utils from "../utils";
|
||||
import { Mode } from "./ui";
|
||||
import i18next from "i18next";
|
||||
import BattleScene from "#app/battle-scene.js";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { addTextObject, TextStyle } from "./text";
|
||||
import { addWindow } from "./ui-theme";
|
||||
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
|
||||
|
||||
export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
private googleImage: Phaser.GameObjects.Image;
|
||||
private discordImage: Phaser.GameObjects.Image;
|
||||
private usernameInfoImage: Phaser.GameObjects.Image;
|
||||
private externalPartyContainer: Phaser.GameObjects.Container;
|
||||
private infoContainer: Phaser.GameObjects.Container;
|
||||
private externalPartyBg: Phaser.GameObjects.NineSlice;
|
||||
private externalPartyTitle: Phaser.GameObjects.Text;
|
||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||
@ -28,17 +31,14 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
this.externalPartyContainer.add(this.externalPartyBg);
|
||||
this.externalPartyContainer.add(this.externalPartyTitle);
|
||||
|
||||
this.infoContainer = this.scene.add.container(0, 0);
|
||||
this.infoContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 12, this.scene.game.canvas.height / 12), Phaser.Geom.Rectangle.Contains);
|
||||
|
||||
const googleImage = this.scene.add.image(0, 0, "google");
|
||||
googleImage.setOrigin(0, 0);
|
||||
googleImage.setScale(0.07);
|
||||
googleImage.setInteractive();
|
||||
googleImage.setName("google-icon");
|
||||
googleImage.on("pointerdown", () => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`);
|
||||
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
|
||||
window.open(googleUrl, "_self");
|
||||
});
|
||||
this.googleImage = googleImage;
|
||||
|
||||
const discordImage = this.scene.add.image(20, 0, "discord");
|
||||
@ -46,12 +46,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
discordImage.setScale(0.07);
|
||||
discordImage.setInteractive();
|
||||
discordImage.setName("discord-icon");
|
||||
discordImage.on("pointerdown", () => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
|
||||
const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID;
|
||||
const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`;
|
||||
window.open(discordUrl, "_self");
|
||||
});
|
||||
|
||||
this.discordImage = discordImage;
|
||||
|
||||
this.externalPartyContainer.add(this.googleImage);
|
||||
@ -60,6 +55,17 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
this.externalPartyContainer.add(this.googleImage);
|
||||
this.externalPartyContainer.add(this.discordImage);
|
||||
this.externalPartyContainer.setVisible(false);
|
||||
|
||||
const usernameInfoImage = this.scene.add.image(20, 0, "settings_icon");
|
||||
usernameInfoImage.setOrigin(0, 0);
|
||||
usernameInfoImage.setScale(0.5);
|
||||
usernameInfoImage.setInteractive();
|
||||
usernameInfoImage.setName("username-info-icon");
|
||||
this.usernameInfoImage = usernameInfoImage;
|
||||
|
||||
this.infoContainer.add(this.usernameInfoImage);
|
||||
this.getUi().add(this.infoContainer);
|
||||
this.infoContainer.setVisible(false);
|
||||
}
|
||||
|
||||
getModalTitle(config?: ModalConfig): string {
|
||||
@ -104,9 +110,8 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
show(args: any[]): boolean {
|
||||
if (super.show(args)) {
|
||||
|
||||
this.processExternalProvider();
|
||||
|
||||
const config = args[0] as ModalConfig;
|
||||
this.processExternalProvider(config);
|
||||
const originalLoginAction = this.submitAction;
|
||||
this.submitAction = (_) => {
|
||||
// Prevent overlapping overrides on action modification
|
||||
@ -146,22 +151,73 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
clear() {
|
||||
super.clear();
|
||||
this.externalPartyContainer.setVisible(false);
|
||||
this.infoContainer.setVisible(false);
|
||||
|
||||
this.discordImage.off("pointerdown");
|
||||
this.googleImage.off("pointerdown");
|
||||
this.usernameInfoImage.off("pointerdown");
|
||||
}
|
||||
|
||||
processExternalProvider() : void {
|
||||
processExternalProvider(config: ModalConfig) : void {
|
||||
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
|
||||
this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length);
|
||||
this.externalPartyTitle.setVisible(true);
|
||||
this.externalPartyContainer.setPositionRelative(this.modalContainer, 175, 0);
|
||||
this.externalPartyContainer.setVisible(true);
|
||||
this.externalPartyBg.setSize(this.externalPartyTitle.text.length+50, this.modalBg.height);
|
||||
this.externalPartyBg.setSize(this.externalPartyTitle.text.length + 50, this.modalBg.height);
|
||||
this.getUi().moveTo(this.externalPartyContainer, this.getUi().length - 1);
|
||||
this.googleImage.setPosition(this.externalPartyBg.width/3.1, this.externalPartyBg.height-60);
|
||||
this.discordImage.setPosition(this.externalPartyBg.width/3.1, this.externalPartyBg.height-40);
|
||||
|
||||
this.infoContainer.setPosition(5, -76);
|
||||
this.infoContainer.setVisible(true);
|
||||
this.getUi().moveTo(this.infoContainer, this.getUi().length - 1);
|
||||
this.usernameInfoImage.setPositionRelative(this.infoContainer, 0, 0);
|
||||
|
||||
this.discordImage.on("pointerdown", () => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
|
||||
const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID;
|
||||
const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`;
|
||||
window.open(discordUrl, "_self");
|
||||
});
|
||||
|
||||
this.googleImage.on("pointerdown", () => {
|
||||
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`);
|
||||
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
|
||||
window.open(googleUrl, "_self");
|
||||
});
|
||||
|
||||
const onFail = error => {
|
||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] });
|
||||
this.scene.ui.setModeForceTransition(Mode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
this.scene.ui.playError();
|
||||
};
|
||||
|
||||
this.usernameInfoImage.on("pointerdown", () => {
|
||||
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage
|
||||
const keyToFind = "data_";
|
||||
const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0);
|
||||
if (dataKeys.length > 0 && dataKeys.length <= 2) {
|
||||
const options: OptionSelectItem[] = [];
|
||||
for (let i = 0; i < dataKeys.length; i++) {
|
||||
options.push({
|
||||
label: dataKeys[i].replace(keyToFind, ""),
|
||||
handler: () => {
|
||||
this.scene.ui.revertMode();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
|
||||
options: options,
|
||||
delay: 1000
|
||||
});
|
||||
} else {
|
||||
return onFail("You have too many save files to use this");
|
||||
}
|
||||
});
|
||||
|
||||
this.externalPartyContainer.setAlpha(0);
|
||||
this.scene.tweens.add({
|
||||
targets: this.externalPartyContainer,
|
||||
@ -170,5 +226,14 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
y: "-=24",
|
||||
alpha: 1
|
||||
});
|
||||
|
||||
this.infoContainer.setAlpha(0);
|
||||
this.scene.tweens.add({
|
||||
targets: this.infoContainer,
|
||||
duration: Utils.fixedInt(1000),
|
||||
ease: "Sine.easeInOut",
|
||||
y: "-=24",
|
||||
alpha: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,20 @@ import { Mode } from "./ui";
|
||||
import { TextStyle, addTextObject } from "./text";
|
||||
import i18next from "i18next";
|
||||
|
||||
|
||||
interface LanguageSetting {
|
||||
inputFieldFontSize?: string,
|
||||
warningMessageFontSize?: string,
|
||||
errorMessageFontSize?: string,
|
||||
}
|
||||
|
||||
const languageSettings: { [key: string]: LanguageSetting } = {
|
||||
"es":{
|
||||
inputFieldFontSize: "50px",
|
||||
errorMessageFontSize: "40px",
|
||||
}
|
||||
};
|
||||
|
||||
export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
getModalTitle(config?: ModalConfig): string {
|
||||
return i18next.t("menu:register");
|
||||
@ -50,7 +64,17 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
setup(): void {
|
||||
super.setup();
|
||||
|
||||
const label = addTextObject(this.scene, 10, 87, i18next.t("menu:registrationAgeWarning"), TextStyle.TOOLTIP_CONTENT, { fontSize: "42px" });
|
||||
this.modalContainer.list.forEach((child: Phaser.GameObjects.GameObject) => {
|
||||
if (child instanceof Phaser.GameObjects.Text && child !== this.titleText) {
|
||||
const inputFieldFontSize = languageSettings[i18next.resolvedLanguage!]?.inputFieldFontSize;
|
||||
if (inputFieldFontSize) {
|
||||
child.setFontSize(inputFieldFontSize);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const warningMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.warningMessageFontSize ?? "42px";
|
||||
const label = addTextObject(this.scene, 10, 87, i18next.t("menu:registrationAgeWarning"), TextStyle.TOOLTIP_CONTENT, { fontSize: warningMessageFontSize});
|
||||
|
||||
this.modalContainer.add(label);
|
||||
}
|
||||
@ -68,6 +92,10 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
const onFail = error => {
|
||||
this.scene.ui.setMode(Mode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
this.scene.ui.playError();
|
||||
const errorMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.errorMessageFontSize;
|
||||
if (errorMessageFontSize) {
|
||||
this.errorMessage.setFontSize(errorMessageFontSize);
|
||||
}
|
||||
};
|
||||
if (!this.inputs[0].text) {
|
||||
return onFail(i18next.t("menu:emptyUsername"));
|
||||
|
Loading…
Reference in New Issue
Block a user