Merge branch 'beta' into toxic_spikes

This commit is contained in:
NightKev 2024-09-05 22:24:58 -07:00 committed by GitHub
commit 631e311372
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 4807 additions and 12982 deletions

View File

@ -2,6 +2,15 @@ pre-commit:
parallel: true parallel: true
commands: commands:
eslint: eslint:
glob: '*.{js,jsx,ts,tsx}' glob: "*.{js,jsx,ts,tsx}"
run: npx eslint --fix {staged_files} 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}

View File

@ -0,0 +1,22 @@
{
"1": {
"529cc5": "8153c7",
"d65a94": "5ad662",
"3a73ad": "6b3aad",
"bd216b": "21bd69",
"5a193a": "195a2a",
"193a63": "391963",
"295a84": "472984"
},
"2": {
"529cc5": "ffedb6",
"d65a94": "e67d2f",
"3a73ad": "ebc582",
"bd216b": "b35131",
"31313a": "3d1519",
"5a193a": "752e2e",
"193a63": "705040",
"295a84": "ad875a",
"4a4a52": "57211a"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1691,8 +1691,8 @@
], ],
"465": [ "465": [
0, 0,
2, 1,
2 1
], ],
"466": [ "466": [
1, 1,
@ -3980,6 +3980,11 @@
1, 1,
1 1
], ],
"465": [
0,
1,
1
],
"592": [ "592": [
1, 1,
1, 1,
@ -5690,7 +5695,7 @@
"465": [ "465": [
0, 0,
1, 1,
2 1
], ],
"466": [ "466": [
2, 2,
@ -8008,6 +8013,11 @@
1, 1,
1 1
], ],
"465": [
0,
1,
1
],
"592": [ "592": [
1, 1,
1, 1,

View File

@ -8,5 +8,14 @@
"bd216b": "21bd69", "bd216b": "21bd69",
"31313a": "31313a", "31313a": "31313a",
"d65a94": "5ad662" "d65a94": "5ad662"
},
"2": {
"5a193a": "752e2e",
"31313a": "3d1519",
"d65a94": "e67d2f",
"3a73ad": "ebc582",
"295a84": "ad875a",
"bd216b": "b35131",
"193a63": "705040"
} }
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,21 @@
{
"1": {
"193a63": "391963",
"295a84": "472984",
"3a73ad": "6b3aad",
"000000": "000000",
"5a193a": "195a2a",
"bd216b": "21bd69",
"31313a": "31313a",
"d65a94": "5ad662"
},
"2": {
"5a193a": "752e2e",
"31313a": "3d1519",
"d65a94": "e67d2f",
"3a73ad": "ebc582",
"295a84": "ad875a",
"bd216b": "b35131",
"193a63": "705040"
}
}

View File

@ -0,0 +1,22 @@
{
"1": {
"529cc5": "8153c7",
"d65a94": "5ad662",
"3a73ad": "6b3aad",
"bd216b": "21bd69",
"5a193a": "195a2a",
"193a63": "391963",
"295a84": "472984"
},
"2": {
"529cc5": "ffedb6",
"d65a94": "e67d2f",
"3a73ad": "ebc582",
"bd216b": "b35131",
"31313a": "3d1519",
"5a193a": "752e2e",
"193a63": "705040",
"295a84": "ad875a",
"4a4a52": "57211a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

View File

@ -855,7 +855,7 @@ export default class BattleScene extends SceneBase {
overrideModifiers(this, false); overrideModifiers(this, false);
overrideHeldItems(this, pokemon, false); overrideHeldItems(this, pokemon, false);
if (boss && !dataSource) { if (boss && !dataSource) {
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295)); const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967296));
for (let s = 0; s < pokemon.ivs.length; s++) { for (let s = 0; s < pokemon.ivs.length; s++) {
pokemon.ivs[s] = Math.round(Phaser.Math.Linear(Math.min(pokemon.ivs[s], secondaryIvs[s]), Math.max(pokemon.ivs[s], secondaryIvs[s]), 0.75)); pokemon.ivs[s] = Math.round(Phaser.Math.Linear(Math.min(pokemon.ivs[s], secondaryIvs[s]), Math.max(pokemon.ivs[s], secondaryIvs[s]), 0.75));
@ -961,6 +961,16 @@ export default class BattleScene extends SceneBase {
this.offsetGym = this.gameMode.isClassic && this.getGeneratedOffsetGym(); this.offsetGym = this.gameMode.isClassic && this.getGeneratedOffsetGym();
} }
/**
* Generates a random number using the current battle's seed
*
* This calls {@linkcode Battle.randSeedInt}(`scene`, {@linkcode range}, {@linkcode min}) in `src/battle.ts`
* which calls {@linkcode Utils.randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`
*
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
* @param min The minimum integer to pick, default `0`
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
*/
randBattleSeedInt(range: integer, min: integer = 0): integer { randBattleSeedInt(range: integer, min: integer = 0): integer {
return this.currentBattle?.randSeedInt(this, range, min); return this.currentBattle?.randSeedInt(this, range, min);
} }
@ -1112,7 +1122,8 @@ export default class BattleScene extends SceneBase {
doubleTrainer = false; doubleTrainer = false;
} }
} }
newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, doubleTrainer ? TrainerVariant.DOUBLE : Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT); const variant = doubleTrainer ? TrainerVariant.DOUBLE : (Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, variant);
this.field.add(newTrainer); this.field.add(newTrainer);
} }
} }
@ -2620,7 +2631,7 @@ export default class BattleScene extends SceneBase {
if (mods.length < 1) { if (mods.length < 1) {
return mods; return mods;
} }
const rand = Math.floor(Utils.randSeedInt(mods.length)); const rand = Utils.randSeedInt(mods.length);
return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))]; return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))];
}; };
modifiers = shuffleModifiers(modifiers); modifiers = shuffleModifiers(modifiers);

View File

@ -6,7 +6,7 @@ import Trainer, { TrainerVariant } from "./field/trainer";
import { GameMode } from "./game-mode"; import { GameMode } from "./game-mode";
import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball"; 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 { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -31,7 +31,7 @@ export enum BattlerIndex {
export interface TurnCommand { export interface TurnCommand {
command: Command; command: Command;
cursor?: integer; cursor?: number;
move?: QueuedMove; move?: QueuedMove;
targets?: BattlerIndex[]; targets?: BattlerIndex[];
skip?: boolean; skip?: boolean;
@ -39,38 +39,40 @@ export interface TurnCommand {
} }
interface TurnCommands { interface TurnCommands {
[key: integer]: TurnCommand | null [key: number]: TurnCommand | null
} }
export default class Battle { export default class Battle {
protected gameMode: GameMode; protected gameMode: GameMode;
public waveIndex: integer; public waveIndex: number;
public battleType: BattleType; public battleType: BattleType;
public battleSpec: BattleSpec; public battleSpec: BattleSpec;
public trainer: Trainer | null; public trainer: Trainer | null;
public enemyLevels: integer[] | undefined; public enemyLevels: number[] | undefined;
public enemyParty: EnemyPokemon[]; public enemyParty: EnemyPokemon[] = [];
public seenEnemyPartyMemberIds: Set<integer>; public seenEnemyPartyMemberIds: Set<number> = new Set<number>();
public double: boolean; public double: boolean;
public started: boolean; public started: boolean = false;
public enemySwitchCounter: integer; public enemySwitchCounter: number = 0;
public turn: integer; public turn: number = 0;
public turnCommands: TurnCommands; public turnCommands: TurnCommands;
public playerParticipantIds: Set<integer>; public playerParticipantIds: Set<number> = new Set<number>();
public battleScore: integer; public battleScore: number = 0;
public postBattleLoot: PokemonHeldItemModifier[]; public postBattleLoot: PokemonHeldItemModifier[] = [];
public escapeAttempts: integer; public escapeAttempts: number = 0;
public lastMove: Moves; public lastMove: Moves;
public battleSeed: string; public battleSeed: string = Utils.randomString(16, true);
private battleSeedState: string | null; private battleSeedState: string | null = null;
public moneyScattered: number; public moneyScattered: number = 0;
public lastUsedPokeball: PokeballType | null; public lastUsedPokeball: PokeballType | null = null;
public playerFaints: number; // The amount of times pokemon on the players side have fainted /** The number of times a Pokemon on the player's side has fainted this battle */
public enemyFaints: number; // The amount of times pokemon on the enemies side have fainted 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.gameMode = gameMode;
this.waveIndex = waveIndex; this.waveIndex = waveIndex;
this.battleType = battleType; this.battleType = battleType;
@ -79,22 +81,7 @@ export default class Battle {
this.enemyLevels = battleType !== BattleType.TRAINER this.enemyLevels = battleType !== BattleType.TRAINER
? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave()) ? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave())
: trainer?.getPartyLevels(this.waveIndex); : trainer?.getPartyLevels(this.waveIndex);
this.enemyParty = []; this.double = double ?? false;
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;
} }
private initBattleSpec(): void { private initBattleSpec(): void {
@ -105,7 +92,7 @@ export default class Battle {
this.battleSpec = spec; this.battleSpec = spec;
} }
private getLevelForWave(): integer { private getLevelForWave(): number {
const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex); const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex);
const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2); const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2);
const bossMultiplier = 1.2; const bossMultiplier = 1.2;
@ -138,7 +125,7 @@ export default class Battle {
return rand / value; return rand / value;
} }
getBattlerCount(): integer { getBattlerCount(): number {
return this.double ? 2 : 1; return this.double ? 2 : 1;
} }
@ -367,7 +354,13 @@ export default class Battle {
return null; return null;
} }
randSeedInt(scene: BattleScene, range: integer, min: integer = 0): integer { /**
* Generates a random number using the current battle's seed. Calls {@linkcode Utils.randSeedInt}
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
* @param min The minimum integer to pick, default `0`
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
*/
randSeedInt(scene: BattleScene, range: number, min: number = 0): number {
if (range <= 1) { if (range <= 1) {
return min; return min;
} }
@ -392,7 +385,7 @@ export default class Battle {
} }
export class FixedBattle extends 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); super(scene.gameMode, waveIndex, config.battleType, config.battleType === BattleType.TRAINER ? config.getTrainer(scene) : undefined, config.double);
if (config.getEnemyParty) { if (config.getEnemyParty) {
this.enemyParty = config.getEnemyParty(scene); this.enemyParty = config.getEnemyParty(scene);
@ -408,7 +401,7 @@ export class FixedBattleConfig {
public double: boolean; public double: boolean;
public getTrainer: GetTrainerFunc; public getTrainer: GetTrainerFunc;
public getEnemyParty: GetEnemyPartyFunc; public getEnemyParty: GetEnemyPartyFunc;
public seedOffsetWaveIndex: integer; public seedOffsetWaveIndex: number;
setBattleType(battleType: BattleType): FixedBattleConfig { setBattleType(battleType: BattleType): FixedBattleConfig {
this.battleType = battleType; this.battleType = battleType;
@ -430,7 +423,7 @@ export class FixedBattleConfig {
return this; return this;
} }
setSeedOffsetWave(seedOffsetWaveIndex: integer): FixedBattleConfig { setSeedOffsetWave(seedOffsetWaveIndex: number): FixedBattleConfig {
this.seedOffsetWaveIndex = seedOffsetWaveIndex; this.seedOffsetWaveIndex = seedOffsetWaveIndex;
return this; return this;
} }
@ -476,7 +469,7 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], rand
} }
export interface FixedBattleConfigs { export interface FixedBattleConfigs {
[key: integer]: FixedBattleConfig [key: number]: FixedBattleConfig
} }
/** /**
* Youngster/Lass on 5 * Youngster/Lass on 5

15
src/data/ability.ts Normal file → Executable file
View 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 { 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 (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) {
if (simulated) { if (simulated) {
return true; return true;
@ -1093,21 +1093,12 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
this.attacker = attacker; this.attacker = attacker;
this.move = move; this.move = move;
this.attacker.addTag(BattlerTagType.DISABLED, 4, 0, pokemon.id);
attacker.summonData.disabledMove = move.id;
attacker.summonData.disabledTurns = 4;
return true; return true;
} }
} }
return false; 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 { export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr {
@ -2651,7 +2642,7 @@ export class ConfusionOnStatusEffectAbAttr extends PostAttackAbAttr {
if (simulated) { if (simulated) {
return defender.canAddTag(BattlerTagType.CONFUSED); return defender.canAddTag(BattlerTagType.CONFUSED);
} else { } else {
return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedInt(3, 2), move.id, defender.id); return defender.addTag(BattlerTagType.CONFUSED, pokemon.randSeedIntRange(2, 5), move.id, defender.id);
} }
} }
return false; return false;

View File

@ -98,6 +98,127 @@ export interface TerrainBattlerTag {
terrainTypes: TerrainType[]; 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. * BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
*/ */
@ -365,7 +486,7 @@ export class ConfusedTag extends BattlerTag {
if (pokemon.randSeedInt(3) === 0) { if (pokemon.randSeedInt(3) === 0) {
const atk = pokemon.getEffectiveStat(Stat.ATK); const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getEffectiveStat(Stat.DEF); const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedInt(15, 85) / 100)); const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage); pokemon.damageAndUpdate(damage);
pokemon.battleData.hitCount++; pokemon.battleData.hitCount++;
@ -1995,6 +2116,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new StockpilingTag(sourceMove); return new StockpilingTag(sourceMove);
case BattlerTagType.OCTOLOCK: case BattlerTagType.OCTOLOCK:
return new OctolockTag(sourceId); return new OctolockTag(sourceId);
case BattlerTagType.DISABLED:
return new DisabledTag(sourceId);
case BattlerTagType.IGNORE_GHOST: case BattlerTagType.IGNORE_GHOST:
return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]); return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]);
case BattlerTagType.IGNORE_DARK: case BattlerTagType.IGNORE_DARK:

View File

@ -15,7 +15,7 @@ export const EGG_SEED = 1073741824;
// Rates for specific random properties in 1/x // Rates for specific random properties in 1/x
const DEFAULT_SHINY_RATE = 128; const DEFAULT_SHINY_RATE = 128;
const GACHA_SHINY_UP_SHINY_RATE = 64; const GACHA_SHINY_UP_SHINY_RATE = 64;
const SAME_SPECIES_EGG_SHINY_RATE = 24; const SAME_SPECIES_EGG_SHINY_RATE = 12;
const SAME_SPECIES_EGG_HA_RATE = 8; const SAME_SPECIES_EGG_HA_RATE = 8;
const MANAPHY_EGG_MANAPHY_RATE = 8; const MANAPHY_EGG_MANAPHY_RATE = 8;
const GACHA_EGG_HA_RATE = 192; const GACHA_EGG_HA_RATE = 192;

View File

@ -4332,72 +4332,6 @@ export class TypelessAttr extends MoveAttr { }
*/ */
export class BypassRedirectAttr 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 { export class FrenzyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.HIT, false, true); super(true, MoveEffectTrigger.HIT, false, true);
@ -4466,7 +4400,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
return (this.selfTarget ? user : target).addTag(this.tagType, user.randSeedInt(this.turnCountMax - this.turnCountMin, this.turnCountMin), move.id, user.id); return (this.selfTarget ? user : target).addTag(this.tagType, user.randSeedIntRange(this.turnCountMin, this.turnCountMax), move.id, user.id);
} }
return false; return false;
@ -4488,6 +4422,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
case BattlerTagType.INFATUATED: case BattlerTagType.INFATUATED:
case BattlerTagType.NIGHTMARE: case BattlerTagType.NIGHTMARE:
case BattlerTagType.DROWSY: case BattlerTagType.DROWSY:
case BattlerTagType.DISABLED:
return -5; return -5;
case BattlerTagType.SEEDED: case BattlerTagType.SEEDED:
case BattlerTagType.SALT_CURED: case BattlerTagType.SALT_CURED:
@ -6299,6 +6234,8 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target:
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(Abilities.COMATOSE); const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(Abilities.COMATOSE);
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined;
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
@ -6673,7 +6610,8 @@ export function initMoves() {
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1) new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
.attr(FixedDamageAttr, 20), .attr(FixedDamageAttr, 20),
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) 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), .condition(failOnMaxCondition),
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
@ -7036,7 +6974,8 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.FREEZE)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(Moves.PROTECT, Type.NORMAL, -1, 10, -1, 4, 2) new SelfStatusMove(Moves.PROTECT, Type.NORMAL, -1, 10, -1, 4, 2)
.attr(ProtectAttr), .attr(ProtectAttr)
.condition(failIfLastCondition),
new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2)
.punchingMove(), .punchingMove(),
new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2)
@ -7087,7 +7026,8 @@ export function initMoves() {
.windMove() .windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(Moves.DETECT, Type.FIGHTING, -1, 5, -1, 4, 2) new SelfStatusMove(Moves.DETECT, Type.FIGHTING, -1, 5, -1, 4, 2)
.attr(ProtectAttr), .attr(ProtectAttr)
.condition(failIfLastCondition),
new AttackMove(Moves.BONE_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 2) new AttackMove(Moves.BONE_RUSH, Type.GROUND, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 2)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
@ -7105,7 +7045,8 @@ export function initMoves() {
.attr(HitHealAttr) .attr(HitHealAttr)
.triageMove(), .triageMove(),
new SelfStatusMove(Moves.ENDURE, Type.NORMAL, -1, 10, -1, 4, 2) new SelfStatusMove(Moves.ENDURE, Type.NORMAL, -1, 10, -1, 4, 2)
.attr(ProtectAttr, BattlerTagType.ENDURING), .attr(ProtectAttr, BattlerTagType.ENDURING)
.condition(failIfLastCondition),
new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2), .attr(StatStageChangeAttr, [ Stat.ATK ], -2),
new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2)
@ -7852,7 +7793,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true),
new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5) new StatusMove(Moves.WIDE_GUARD, Type.ROCK, -1, 10, -1, 3, 5)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true), .attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true)
.condition(failIfLastCondition),
new StatusMove(Moves.GUARD_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(Moves.GUARD_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5)
.attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"), .attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"),
new StatusMove(Moves.POWER_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(Moves.POWER_SPLIT, Type.PSYCHIC, -1, 10, -1, 0, 5)
@ -7940,7 +7882,8 @@ export function initMoves() {
.attr(PositiveStatStagePowerAttr), .attr(PositiveStatStagePowerAttr),
new StatusMove(Moves.QUICK_GUARD, Type.FIGHTING, -1, 15, -1, 3, 5) new StatusMove(Moves.QUICK_GUARD, Type.FIGHTING, -1, 15, -1, 3, 5)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true), .attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true)
.condition(failIfLastCondition),
new SelfStatusMove(Moves.ALLY_SWITCH, Type.PSYCHIC, -1, 15, -1, 2, 5) new SelfStatusMove(Moves.ALLY_SWITCH, Type.PSYCHIC, -1, 15, -1, 2, 5)
.ignoresProtect() .ignoresProtect()
.unimplemented(), .unimplemented(),
@ -8111,7 +8054,8 @@ export function initMoves() {
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true) .attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true)
.condition(new FirstMoveCondition()), .condition(new FirstMoveCondition())
.condition(failIfLastCondition),
new AttackMove(Moves.BELCH, Type.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6) new AttackMove(Moves.BELCH, Type.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6)
.condition((user, target, move) => user.battleData.berriesEaten.length > 0), .condition((user, target, move) => user.battleData.berriesEaten.length > 0),
new StatusMove(Moves.ROTOTILLER, Type.GROUND, -1, 10, -1, 0, 6) new StatusMove(Moves.ROTOTILLER, Type.GROUND, -1, 10, -1, 0, 6)
@ -8169,7 +8113,8 @@ export function initMoves() {
.triageMove(), .triageMove(),
new StatusMove(Moves.CRAFTY_SHIELD, Type.FAIRY, -1, 10, -1, 3, 6) new StatusMove(Moves.CRAFTY_SHIELD, Type.FAIRY, -1, 10, -1, 3, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true), .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true)
.condition(failIfLastCondition),
new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6)
.target(MoveTarget.ALL) .target(MoveTarget.ALL)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)),
@ -8194,7 +8139,8 @@ export function initMoves() {
.target(MoveTarget.BOTH_SIDES) .target(MoveTarget.BOTH_SIDES)
.unimplemented(), .unimplemented(),
new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6) new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.KINGS_SHIELD), .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD)
.condition(failIfLastCondition),
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1), .attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
@ -8217,7 +8163,8 @@ export function initMoves() {
new AttackMove(Moves.MYSTICAL_FIRE, Type.FIRE, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 6) new AttackMove(Moves.MYSTICAL_FIRE, Type.FIRE, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1), .attr(StatStageChangeAttr, [ Stat.SPATK ], -1),
new SelfStatusMove(Moves.SPIKY_SHIELD, Type.GRASS, -1, 10, -1, 4, 6) new SelfStatusMove(Moves.SPIKY_SHIELD, Type.GRASS, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD), .attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD)
.condition(failIfLastCondition),
new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6) new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1)
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
@ -8413,7 +8360,8 @@ export function initMoves() {
new AttackMove(Moves.FIRST_IMPRESSION, Type.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, 2, 7) new AttackMove(Moves.FIRST_IMPRESSION, Type.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, 2, 7)
.condition(new FirstMoveCondition()), .condition(new FirstMoveCondition()),
new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7) new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7)
.attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER), .attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER)
.condition(failIfLastCondition),
new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7) new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
.makesContact(false), .makesContact(false),
@ -8656,6 +8604,7 @@ export function initMoves() {
/* Unused */ /* Unused */
new SelfStatusMove(Moves.MAX_GUARD, Type.NORMAL, -1, 10, -1, 4, 8) new SelfStatusMove(Moves.MAX_GUARD, Type.NORMAL, -1, 10, -1, 4, 8)
.attr(ProtectAttr) .attr(ProtectAttr)
.condition(failIfLastCondition)
.ignoresVirtual(), .ignoresVirtual(),
/* End Unused */ /* End Unused */
new AttackMove(Moves.DYNAMAX_CANNON, Type.DRAGON, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) new AttackMove(Moves.DYNAMAX_CANNON, Type.DRAGON, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8)
@ -8834,7 +8783,8 @@ export function initMoves() {
.target(MoveTarget.USER_AND_ALLIES) .target(MoveTarget.USER_AND_ALLIES)
.ignoresProtect(), .ignoresProtect(),
new SelfStatusMove(Moves.OBSTRUCT, Type.DARK, 100, 10, -1, 4, 8) new SelfStatusMove(Moves.OBSTRUCT, Type.DARK, 100, 10, -1, 4, 8)
.attr(ProtectAttr, BattlerTagType.OBSTRUCT), .attr(ProtectAttr, BattlerTagType.OBSTRUCT)
.condition(failIfLastCondition),
new AttackMove(Moves.FALSE_SURRENDER, Type.DARK, MoveCategory.PHYSICAL, 80, -1, 10, -1, 0, 8), new AttackMove(Moves.FALSE_SURRENDER, Type.DARK, MoveCategory.PHYSICAL, 80, -1, 10, -1, 0, 8),
new AttackMove(Moves.METEOR_ASSAULT, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 5, -1, 0, 8) new AttackMove(Moves.METEOR_ASSAULT, Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 5, -1, 0, 8)
.attr(RechargeAttr) .attr(RechargeAttr)
@ -9122,10 +9072,10 @@ export function initMoves() {
.attr(TeraBlastCategoryAttr) .attr(TeraBlastCategoryAttr)
.attr(TeraBlastTypeAttr) .attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr) .attr(TeraBlastPowerAttr)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)),
.partial(),
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP), .attr(ProtectAttr, BattlerTagType.SILK_TRAP)
.condition(failIfLastCondition),
new AttackMove(Moves.AXE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 90, 10, 30, 0, 9) new AttackMove(Moves.AXE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 90, 10, 30, 0, 9)
.attr(MissEffectAttr, crashDamageFunc) .attr(MissEffectAttr, crashDamageFunc)
.attr(NoEffectAttr, crashDamageFunc) .attr(NoEffectAttr, crashDamageFunc)
@ -9317,7 +9267,8 @@ export function initMoves() {
.attr(PreMoveMessageAttr, doublePowerChanceMessageFunc) .attr(PreMoveMessageAttr, doublePowerChanceMessageFunc)
.attr(DoublePowerChanceAttr), .attr(DoublePowerChanceAttr),
new SelfStatusMove(Moves.BURNING_BULWARK, Type.FIRE, -1, 10, -1, 4, 9) new SelfStatusMove(Moves.BURNING_BULWARK, Type.FIRE, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.BURNING_BULWARK), .attr(ProtectAttr, BattlerTagType.BURNING_BULWARK)
.condition(failIfLastCondition),
new AttackMove(Moves.THUNDERCLAP, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 5, -1, 1, 9) new AttackMove(Moves.THUNDERCLAP, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 5, -1, 1, 9)
.condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct? .condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct?
new AttackMove(Moves.MIGHTY_CLEAVE, Type.ROCK, MoveCategory.PHYSICAL, 95, 100, 5, -1, 0, 9) new AttackMove(Moves.MIGHTY_CLEAVE, Type.ROCK, MoveCategory.PHYSICAL, 95, 100, 5, -1, 0, 9)

View File

@ -64,6 +64,7 @@ export enum BattlerTagType {
STOCKPILING = "STOCKPILING", STOCKPILING = "STOCKPILING",
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
ALWAYS_GET_HIT = "ALWAYS_GET_HIT", ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
DISABLED = "DISABLED",
IGNORE_GHOST = "IGNORE_GHOST", IGNORE_GHOST = "IGNORE_GHOST",
IGNORE_DARK = "IGNORE_DARK", IGNORE_DARK = "IGNORE_DARK",
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",

View File

@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; 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 { WeatherType } from "../data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; 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"; 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";
@ -1720,7 +1720,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}; };
this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true); this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true);
this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0); this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? 2 : this.fusionSpecies.ability2 !== this.fusionSpecies.ability1 ? randAbilityIndex : 0);
this.fusionShiny = this.shiny; this.fusionShiny = this.shiny;
this.fusionVariant = this.variant; this.fusionVariant = this.variant;
@ -2278,7 +2278,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!isTypeImmune) { if (!isTypeImmune) {
const levelMultiplier = (2 * source.level / 5 + 2); const levelMultiplier = (2 * source.level / 5 + 2);
const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100); const randomMultiplier = (this.randSeedIntRange(85, 100) / 100);
damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2) damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
* stabMultiplier.value * stabMultiplier.value
* typeMultiplier * typeMultiplier
@ -2670,6 +2670,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.updateInfo(); 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[] { getMoveHistory(): TurnMove[] {
return this.battleSummonData.moveHistory; return this.battleSummonData.moveHistory;
} }
@ -3421,12 +3448,30 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
fusionCanvas.remove(); fusionCanvas.remove();
} }
/**
* Generates a random number using the current battle's seed, or the global seed if `this.scene.currentBattle` is falsy
* <!-- @import "../battle".Battle -->
* This calls either {@linkcode BattleScene.randBattleSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle-scene.ts`
* which calls {@linkcode Battle.randSeedInt}(`scene`, {@linkcode range}, {@linkcode min}) in `src/battle.ts`
* which calls {@linkcode Utils.randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`,
* or it directly calls {@linkcode Utils.randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts` if there is no current battle
*
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
* @param min The minimum integer to pick, default `0`
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
*/
randSeedInt(range: integer, min: integer = 0): integer { randSeedInt(range: integer, min: integer = 0): integer {
return this.scene.currentBattle return this.scene.currentBattle
? this.scene.randBattleSeedInt(range, min) ? this.scene.randBattleSeedInt(range, min)
: Utils.randSeedInt(range, min); : Utils.randSeedInt(range, min);
} }
/**
* Generates a random number using the current battle's seed, or the global seed if `this.scene.currentBattle` is falsy
* @param min The minimum integer to generate
* @param max The maximum integer to generate
* @returns a random integer between {@linkcode min} and {@linkcode max} inclusive
*/
randSeedIntRange(min: integer, max: integer): integer { randSeedIntRange(min: integer, max: integer): integer {
return this.randSeedInt((max - min) + 1, min); return this.randSeedInt((max - min) + 1, min);
} }
@ -4458,8 +4503,6 @@ export interface AttackMoveResult {
export class PokemonSummonData { export class PokemonSummonData {
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public moveQueue: QueuedMove[] = []; public moveQueue: QueuedMove[] = [];
public disabledMove: Moves = Moves.NONE;
public disabledTurns: number = 0;
public tags: BattlerTag[] = []; public tags: BattlerTag[] = [];
public abilitySuppressed: boolean = false; public abilitySuppressed: boolean = false;
public abilitiesApplied: Abilities[] = []; public abilitiesApplied: Abilities[] = [];
@ -4540,7 +4583,7 @@ export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | Hit
* It links to {@linkcode Move} class via the move ID. * It links to {@linkcode Move} class via the move ID.
* Compared to {@linkcode Move}, this class also tracks if a move has received. * Compared to {@linkcode Move}, this class also tracks if a move has received.
* PP Ups, amount of PP used, and things like that. * 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 getMove} - returns {@linkcode Move} object by looking it up via ID.
* @see {@linkcode usePp} - removes a point of PP from the move. * @see {@linkcode usePp} - removes a point of PP from the move.
* @see {@linkcode getMovePp} - returns amount of PP a move currently has. * @see {@linkcode getMovePp} - returns amount of PP a move currently has.
@ -4560,11 +4603,25 @@ export class PokemonMove {
this.virtual = !!virtual; 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 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 { getMove(): Move {

View File

@ -165,6 +165,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("saving_icon", "ui"); this.loadImage("saving_icon", "ui");
this.loadImage("discord", "ui"); this.loadImage("discord", "ui");
this.loadImage("google", "ui"); this.loadImage("google", "ui");
this.loadImage("settings_icon", "ui");
this.loadImage("default_bg", "arenas"); this.loadImage("default_bg", "arenas");
// Load arena images // Load arena images

View File

@ -36,5 +36,6 @@
"matBlock": "Tatami-Schild", "matBlock": "Tatami-Schild",
"craftyShield": "Trickschutz", "craftyShield": "Trickschutz",
"tailwind": "Rückenwind", "tailwind": "Rückenwind",
"happyHour": "Goldene Zeiten" "happyHour": "Goldene Zeiten",
} "safeguard": "Bodyguard"
}

View File

@ -44,6 +44,7 @@
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.", "moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
"moveNoPP": "There's no PP left for\nthis move!", "moveNoPP": "There's no PP left for\nthis move!",
"moveDisabled": "{{moveName}} is disabled!", "moveDisabled": "{{moveName}} is disabled!",
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
"noPokeballForce": "An unseen force\nprevents using Poké Balls.", "noPokeballForce": "An unseen force\nprevents using Poké Balls.",
"noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!", "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!", "noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!",

View File

@ -67,5 +67,7 @@
"saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!", "saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
"cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!", "cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
"cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!", "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."
}

View File

@ -51,5 +51,7 @@
"renamePokemon": "Rename Pokémon", "renamePokemon": "Rename Pokémon",
"rename": "Rename", "rename": "Rename",
"nickname": "Nickname", "nickname": "Nickname",
"errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect." "errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect.",
"noSaves": "You don't have any save files on record!",
"tooManySaves": "You have too many save files on record!"
} }

View File

@ -36,5 +36,6 @@
"matBlock": "Escudo Tatami", "matBlock": "Escudo Tatami",
"craftyShield": "Truco Defensa", "craftyShield": "Truco Defensa",
"tailwind": "Viento Afín", "tailwind": "Viento Afín",
"happyHour": "Paga Extra" "happyHour": "Paga Extra",
"safeguard": "Velo Sagrado"
} }

View File

@ -14,14 +14,14 @@
"register": "Registrarse", "register": "Registrarse",
"emptyUsername": "El usuario no puede estar vacío", "emptyUsername": "El usuario no puede estar vacío",
"invalidLoginUsername": "El usuario no es válido", "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", "invalidLoginPassword": "La contraseña no es válida",
"invalidRegisterPassword": "La contraseña debe tener 6 o más caracteres.", "invalidRegisterPassword": "La contraseña debe tener 6 o más caracteres.",
"usernameAlreadyUsed": "El usuario ya está en uso", "usernameAlreadyUsed": "El usuario ya está en uso",
"accountNonExistent": "El usuario no existe", "accountNonExistent": "El usuario no existe",
"unmatchingPassword": "La contraseña no coincide", "unmatchingPassword": "La contraseña no coincide",
"passwordNotMatchingConfirmPassword": "Las contraseñas deben coincidir", "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.", "registrationAgeWarning": "Al registrarte, confirmas tener 13 o más años de edad.",
"backToLogin": "Volver al Login", "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.", "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.",

View File

@ -36,5 +36,6 @@
"matBlock": "Tatamigaeshi", "matBlock": "Tatamigaeshi",
"craftyShield": "Vigilance", "craftyShield": "Vigilance",
"tailwind": "Vent Arrière", "tailwind": "Vent Arrière",
"happyHour": "Étrennes" "happyHour": "Étrennes",
} "safeguard": "Rune Protect"
}

View File

@ -36,5 +36,6 @@
"matBlock": "Ribaltappeto", "matBlock": "Ribaltappeto",
"craftyShield": "Truccodifesa", "craftyShield": "Truccodifesa",
"tailwind": "Ventoincoda", "tailwind": "Ventoincoda",
"happyHour": "Cuccagna" "happyHour": "Cuccagna",
} "safeguard": "Salvaguardia"
}

View File

@ -37,7 +37,7 @@
"name_female": "ワンパンウーマン" "name_female": "ワンパンウーマン"
}, },
"HealAchv": { "HealAchv": {
"description": "一つの 技や 特性や 持っているアイテムで\n{{healAmount}}{{HP}}を 一気に 回復する" "description": "一つの 技や 特性や 持たせたアイテムで\n{{HP}}{{healAmount}}を 一気に 回復する"
}, },
"250_HEAL": { "250_HEAL": {
"name": "回復発見者" "name": "回復発見者"
@ -52,7 +52,7 @@
"name": "ジョーイさん" "name": "ジョーイさん"
}, },
"LevelAchv": { "LevelAchv": {
"description": "一つの ポケモンを Lv{{level}}まで レベルアップする" "description": "一つの ポケモンを Lv.{{level}}まで 上げる"
}, },
"LV_100": { "LV_100": {
"name": "まだまだだよ" "name": "まだまだだよ"
@ -82,7 +82,7 @@
"name": "マスターリーグチャンピオン" "name": "マスターリーグチャンピオン"
}, },
"TRANSFER_MAX_STAT_STAGE": { "TRANSFER_MAX_STAT_STAGE": {
"name": "同力", "name": "連係プレー",
"description": "少なくとも 一つの 能力を 最大まで あげて\n他の 手持ちポケモンに バトンタッチする" "description": "少なくとも 一つの 能力を 最大まで あげて\n他の 手持ちポケモンに バトンタッチする"
}, },
"MAX_FRIENDSHIP": { "MAX_FRIENDSHIP": {
@ -94,7 +94,7 @@
"description": "一つの 手持ちポケモンを メガシンカさせる" "description": "一つの 手持ちポケモンを メガシンカさせる"
}, },
"GIGANTAMAX": { "GIGANTAMAX": {
"name": "太―くて 堪らない", "name": "太ーくて堪らない",
"description": "一つの 手持ちポケモンを キョダイマックスさせる" "description": "一つの 手持ちポケモンを キョダイマックスさせる"
}, },
"TERASTALLIZE": { "TERASTALLIZE": {
@ -106,7 +106,7 @@
"description": "一つの 手持ちポケモンを ステラ・テラスタルさせる" "description": "一つの 手持ちポケモンを ステラ・テラスタルさせる"
}, },
"SPLICE": { "SPLICE": {
"name": "インフィニット・フュジョン", "name": "インフィニット・フュジョン",
"description": "遺伝子のくさびで 二つの ポケモンを 吸収合体させる" "description": "遺伝子のくさびで 二つの ポケモンを 吸収合体させる"
}, },
"MINI_BLACK_HOLE": { "MINI_BLACK_HOLE": {
@ -205,7 +205,7 @@
"description": "{{type}}タイプの 単一タイプチャレンジを クリアする" "description": "{{type}}タイプの 単一タイプチャレンジを クリアする"
}, },
"MONO_NORMAL": { "MONO_NORMAL": {
"name": "凡人" "name": "凡人"
}, },
"MONO_FIGHTING": { "MONO_FIGHTING": {
"name": "八千以上だ!!" "name": "八千以上だ!!"
@ -223,7 +223,7 @@
"name": "タケシの挑戦状" "name": "タケシの挑戦状"
}, },
"MONO_BUG": { "MONO_BUG": {
"name": "チョウチョウせん者" "name": "チョウチョウ者"
}, },
"MONO_GHOST": { "MONO_GHOST": {
"name": "貞子ちゃん" "name": "貞子ちゃん"

View File

@ -1 +1,43 @@
{} {
"activeBattleEffects": "場の効果",
"player": "味方",
"neutral": "場の全員",
"enemy": "相手",
"sunny": "晴れ",
"rain": "雨",
"sandstorm": "砂あらし",
"hail": "あられ",
"snow": "雪",
"fog": "きり",
"heavyRain": "強い雨",
"harshSun": "大日照り",
"strongWinds": "乱気流",
"misty": "ミストフィールド",
"electric": "エレキフィールド",
"grassy": "グラスフィールド",
"psychic": "サイコフィールド",
"mudSport": "どろあそび",
"waterSport": "みずあそび",
"spikes": "まきびし",
"toxicSpikes": "どくびし",
"mist": "しろいきり",
"futureSight": "みらいよち",
"doomDesire": "はめつのねがい",
"wish": "ねがいごと",
"stealthRock": "ステルスロック",
"stickyWeb": "ねばねばネット",
"trickRoom": "トリックルーム",
"gravity": "じゅうりょく",
"reflect": "リフレクター",
"lightScreen": "ひかりのかべ",
"auroraVeil": "オーロラベール",
"quickGuard": "ファストガード",
"wideGuard": "ワイドガード",
"matBlock": "たたみがえし",
"craftyShield": "トリックガード",
"tailwind": "おいかぜ",
"happyHour": "ハッピータイム"
}

View File

@ -1 +1,150 @@
{} {
"music": "Music: ",
"missing_entries": "{{name}}",
"battle_kanto_champion": "B2W2 Kanto Champion Battle",
"battle_johto_champion": "B2W2 Johto Champion Battle",
"battle_hoenn_champion_g5": "B2W2 Hoenn Champion Battle",
"battle_hoenn_champion_g6": "ORAS Hoenn Champion Battle",
"battle_sinnoh_champion": "B2W2 Sinnoh Champion Battle",
"battle_champion_alder": "BW Unova Champion Battle",
"battle_champion_iris": "B2W2 Unova Champion Battle",
"battle_kalos_champion": "XY Kalos Champion Battle",
"battle_alola_champion": "USUM Alola Champion Battle",
"battle_galar_champion": "SWSH Galar Champion Battle",
"battle_champion_geeta": "SV Champion Geeta Battle",
"battle_champion_nemona": "SV Champion Nemona Battle",
"battle_champion_kieran": "SV Champion Kieran Battle",
"battle_hoenn_elite": "ORAS Elite Four Battle",
"battle_unova_elite": "BW Elite Four Battle",
"battle_kalos_elite": "XY Elite Four Battle",
"battle_alola_elite": "SM Elite Four Battle",
"battle_galar_elite": "SWSH League Tournament Battle",
"battle_paldea_elite": "SV Elite Four Battle",
"battle_bb_elite": "SV BB League Elite Four Battle",
"battle_final_encounter": "PMD RTDX Rayquaza's Domain",
"battle_final": "BW Ghetsis Battle",
"battle_kanto_gym": "B2W2 Kanto Gym Battle",
"battle_johto_gym": "B2W2 Johto Gym Battle",
"battle_hoenn_gym": "B2W2 Hoenn Gym Battle",
"battle_sinnoh_gym": "B2W2 Sinnoh Gym Battle",
"battle_unova_gym": "BW Unova Gym Battle",
"battle_kalos_gym": "XY Kalos Gym Battle",
"battle_galar_gym": "SWSH Galar Gym Battle",
"battle_paldea_gym": "SV Paldea Gym Battle",
"battle_legendary_kanto": "XY Kanto Legendary Battle",
"battle_legendary_raikou": "HGSS Raikou Battle",
"battle_legendary_entei": "HGSS Entei Battle",
"battle_legendary_suicune": "HGSS Suicune Battle",
"battle_legendary_lugia": "HGSS Lugia Battle",
"battle_legendary_ho_oh": "HGSS Ho-oh Battle",
"battle_legendary_regis_g5": "B2W2 Legendary Titan Battle",
"battle_legendary_regis_g6": "ORAS Legendary Titan Battle",
"battle_legendary_gro_kyo": "ORAS Groudon & Kyogre Battle",
"battle_legendary_rayquaza": "ORAS Rayquaza Battle",
"battle_legendary_deoxys": "ORAS Deoxys Battle",
"battle_legendary_lake_trio": "ORAS Lake Guardians Battle",
"battle_legendary_sinnoh": "ORAS Sinnoh Legendary Battle",
"battle_legendary_dia_pal": "ORAS Dialga & Palkia Battle",
"battle_legendary_origin_forme": "LA Origin Dialga & Palkia Battle",
"battle_legendary_giratina": "ORAS Giratina Battle",
"battle_legendary_arceus": "HGSS Arceus Battle",
"battle_legendary_unova": "BW Unova Legendary Battle",
"battle_legendary_kyurem": "BW Kyurem Battle",
"battle_legendary_res_zek": "BW Reshiram & Zekrom Battle",
"battle_legendary_xern_yvel": "XY Xerneas & Yveltal Battle",
"battle_legendary_tapu": "SM Tapu Battle",
"battle_legendary_sol_lun": "SM Solgaleo & Lunala Battle",
"battle_legendary_ub": "SM Ultra Beast Battle",
"battle_legendary_dusk_dawn": "USUM Dusk Mane & Dawn Wings Necrozma Battle",
"battle_legendary_ultra_nec": "USUM Ultra Necrozma Battle",
"battle_legendary_zac_zam": "SWSH Zacian & Zamazenta Battle",
"battle_legendary_glas_spec": "SWSH Glastrier & Spectrier Battle",
"battle_legendary_calyrex": "SWSH Calyrex Battle",
"battle_legendary_riders": "SWSH Ice & Shadow Rider Calyrex Battle",
"battle_legendary_birds_galar": "SWSH Galarian Legendary Birds Battle",
"battle_legendary_ruinous": "SV Treasures of Ruin Battle",
"battle_legendary_kor_mir": "SV Depths of Area Zero Battle",
"battle_legendary_loyal_three": "SV Loyal Three Battle",
"battle_legendary_ogerpon": "SV Ogerpon Battle",
"battle_legendary_terapagos": "SV Terapagos Battle",
"battle_legendary_pecharunt": "SV Pecharunt Battle",
"battle_rival": "BW Rival Battle",
"battle_rival_2": "BW N Battle",
"battle_rival_3": "BW Final N Battle",
"battle_trainer": "BW Trainer Battle",
"battle_wild": "BW Wild Battle",
"battle_wild_strong": "BW Strong Wild Battle",
"end_summit": "PMD RTDX Sky Tower Summit",
"battle_rocket_grunt": "HGSS Team Rocket Battle",
"battle_aqua_magma_grunt": "ORAS Team Aqua & Magma Battle",
"battle_galactic_grunt": "BDSP Team Galactic Battle",
"battle_plasma_grunt": "BW Team Plasma Battle",
"battle_flare_grunt": "XY Team Flare Battle",
"battle_aether_grunt": "SM Aether Foundation Battle",
"battle_skull_grunt": "SM Team Skull Battle",
"battle_macro_grunt": "SWSH Trainer Battle",
"battle_galactic_admin": "BDSP Team Galactic Admin Battle",
"battle_skull_admin": "SM Team Skull Admin Battle",
"battle_oleana": "SWSH Oleana Battle",
"battle_rocket_boss": "USUM Giovanni Battle",
"battle_aqua_magma_boss": "ORAS Archie & Maxie Battle",
"battle_galactic_boss": "BDSP Cyrus Battle",
"battle_plasma_boss": "B2W2 Ghetsis Battle",
"battle_flare_boss": "XY Lysandre Battle",
"battle_aether_boss": "SM Lusamine Battle",
"battle_skull_boss": "SM Guzma Battle",
"battle_macro_boss": "SWSH Rose Battle",
"abyss": "PMD EoS Dark Crater",
"badlands": "PMD EoS Barren Valley",
"beach": "PMD EoS Drenched Bluff",
"cave": "PMD EoS Sky Peak Cave",
"construction_site": "PMD EoS Boulder Quarry",
"desert": "PMD EoS Northern Desert",
"dojo": "PMD EoS Marowak Dojo",
"end": "PMD RTDX Sky Tower",
"factory": "PMD EoS Concealed Ruins",
"fairy_cave": "PMD EoS Star Cave",
"forest": "PMD EoS Dusk Forest",
"grass": "PMD EoS Apple Woods",
"graveyard": "PMD EoS Mystifying Forest",
"ice_cave": "PMD EoS Vast Ice Mountain",
"island": "PMD EoS Craggy Coast",
"jungle": "Lmz - Jungle",
"laboratory": "Firel - Laboratory",
"lake": "PMD EoS Crystal Cave",
"meadow": "PMD EoS Sky Peak Forest",
"metropolis": "Firel - Metropolis",
"mountain": "PMD EoS Mt. Horn",
"plains": "PMD EoS Sky Peak Prairie",
"power_plant": "PMD EoS Far Amp Plains",
"ruins": "PMD EoS Deep Sealed Ruin",
"sea": "Andr06 - Marine Mystique",
"seabed": "Firel - Seabed",
"slum": "Andr06 - Sneaky Snom",
"snowy_forest": "PMD EoS Sky Peak Snowfield",
"space": "Firel - Aether",
"swamp": "PMD EoS Surrounded Sea",
"tall_grass": "PMD EoS Foggy Forest",
"temple": "PMD EoS Aegis Cave",
"town": "PMD EoS Random Dungeon Theme 3",
"volcano": "PMD EoS Steam Cave",
"wasteland": "PMD EoS Hidden Highland",
"encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)",
"encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)",
"encounter_clerk": "BW Trainers' Eyes Meet (Clerk)",
"encounter_cyclist": "BW Trainers' Eyes Meet (Cyclist)",
"encounter_lass": "BW Trainers' Eyes Meet (Lass)",
"encounter_parasol_lady": "BW Trainers' Eyes Meet (Parasol Lady)",
"encounter_pokefan": "BW Trainers' Eyes Meet (Poke Fan)",
"encounter_psychic": "BW Trainers' Eyes Meet (Psychic)",
"encounter_rich": "BW Trainers' Eyes Meet (Gentleman)",
"encounter_rival": "BW Cheren",
"encounter_roughneck": "BW Trainers' Eyes Meet (Roughneck)",
"encounter_scientist": "BW Trainers' Eyes Meet (Scientist)",
"encounter_twins": "BW Trainers' Eyes Meet (Twins)",
"encounter_youngster": "BW Trainers' Eyes Meet (Youngster)",
"heal": "BW Pokémon Heal",
"menu": "PMD EoS Welcome to the World of Pokémon!",
"title": "PMD EoS Top Menu Theme"
}

View File

@ -1,5 +1,5 @@
{ {
"title": "チャレンジ設定", "title": "チャレンジ設定",
"illegalEvolution": "{{pokemon}}は このチャレンジで\n対象外の ポケモンに なってしまった", "illegalEvolution": "{{pokemon}}は このチャレンジで\n対象外の ポケモンに なってしまった",
"singleGeneration": { "singleGeneration": {
"name": "単一世代", "name": "単一世代",

View File

@ -1 +1,8 @@
{} {
"start": "スタート",
"luckIndicator": "運:",
"shinyOnHover": "色違い",
"commonShiny": "ふつう",
"rareShiny": "レア",
"epicShiny": "超レア"
}

View File

@ -1 +1,84 @@
{} {
"blue_red_double": {
"encounter": {
"1": "Blue: Hey Red, let's show them what we're made of!\n$Red: ...\n$Blue: This is Pallet Town Power!"
},
"victory": {
"1": "Blue: That was a great battle!\n$Red: ..."
}
},
"red_blue_double": {
"encounter": {
"1": "Red: ...!\n$Blue: He never talks much.\n$Blue: But dont let that fool you! He is a champ after all!"
},
"victory": {
"1": "Red: ...!\n$Blue: Next time we will beat you!"
}
},
"tate_liza_double": {
"encounter": {
"1": "Tate: Are you surprised?\n$Liza: We are two gym leaders at once!\n$Tate: We are twins!\n$Liza: We dont need to talk to understand each other!\n$Tate: Twice the power...\n$Liza: Can you handle it?"
},
"victory": {
"1": "Tate: What? Our combination was perfect!\n$Liza: Looks like we need to train more..."
}
},
"liza_tate_double": {
"encounter": {
"1": "Liza: Hihihi... Are you surprised?\n$Tate: Yes, we are really two gym leaders at once!\n$Liza: This is my twin brother Tate!\n$Tate: And this is my twin sister Liza!\n$Liza: Don't you think we are a perfect combination?"
},
"victory": {
"1": "Liza: Are we...\n$Tate: ...not as strong as we thought?"
}
},
"wallace_steven_double": {
"encounter": {
"1": "Steven: Wallace, let's show them the power of the champions!\n$Wallace: We will show you the power of Hoenn!\n$Steven: Let's go!"
},
"victory": {
"1": "Steven: That was a great battle!\n$Wallace: We will win next time!"
}
},
"steven_wallace_double": {
"encounter": {
"1": "Steven: Do you have any rare Pokémon?\n$Wallace: Steven... We are here for a battle, not to show off our Pokémon.\n$Steven: Oh... I see... Let's go then!"
},
"victory": {
"1": "Steven: Now that we are done with the battle, let's show off our Pokémon!\n$Wallace: Steven..."
}
},
"alder_iris_double": {
"encounter": {
"1": "Alder: We are the strongest trainers in Unova!\n$Iris: Fights against strong trainers are the best!"
},
"victory": {
"1": "Alder: Wow! You are super strong!\n$Iris: We will win next time!"
}
},
"iris_alder_double": {
"encounter": {
"1": "Iris: Welcome Challenger! I am THE Unova Champion!\n$Alder: Iris, aren't you a bit too excited?",
"1_female": "Iris: Welcome Challenger! I am THE Unova Champion!\n$Alder: Iris, aren't you a bit too excited?"
},
"victory": {
"1": "Iris: A loss like this is not easy to take...\n$Alder: But we will only get stronger with every loss!"
}
},
"piers_marnie_double": {
"encounter": {
"1": "Marnie: Brother, let's show them the power of Spikemuth!\n$Piers: We bring darkness!"
},
"victory": {
"1": "Marnie: You brought light to our darkness!\n$Piers: Its too bright..."
}
},
"marnie_piers_double": {
"encounter": {
"1": "Piers: Ready for a concert?\n$Marnie: Brother... They are here to fight, not to sing...",
"1_female": "Piers: Ready for a concert?\n$Marnie: Brother... They are here to fight, not to sing..."
},
"victory": {
"1": "Piers: Now that was a great concert!\n$Marnie: Brother..."
}
}
}

View File

@ -1 +1,10 @@
{} {
"encounter": "It appears the time has finally come once again.\nYou know why you have come here, do you not?\n$You were drawn here, because you have been here before.\nCountless times.\n$Though, perhaps it can be counted.\nTo be precise, this is in fact your {{cycleCount}} cycle.\n$Each cycle your mind reverts to its former state.\nEven so, somehow, remnants of your former selves remain.\n$Until now you have yet to succeed, but I sense a different presence in you this time.\n\n$You are the only one here, though it is as if there is… another.\n$Will you finally prove a formidable challenge to me?\nThe challenge I have longed after for millennia?\n$We begin.",
"encounter_female": "It appears the time has finally come once again.\nYou know why you have come here, do you not?\n$You were drawn here, because you have been here before.\nCountless times.\n$Though, perhaps it can be counted.\nTo be precise, this is in fact your {{cycleCount}} cycle.\n$Each cycle your mind reverts to its former state.\nEven so, somehow, remnants of your former selves remain.\n$Until now you have yet to succeed, but I sense a different presence in you this time.\n\n$You are the only one here, though it is as if there is… another.\n$Will you finally prove a formidable challenge to me?\nThe challenge I have longed after for millennia?\n$We begin.",
"firstStageWin": "I see. The presence I felt was indeed real.\nIt appears I no longer need to hold back.\n$Do not disappoint me.",
"secondStageWin": "…Magnificent.",
"key_ordinal_one": "st",
"key_ordinal_two": "nd",
"key_ordinal_few": "rd",
"key_ordinal_other": "th"
}

View File

@ -1 +1,6 @@
{} {
"ending": "@c{shock}You're back?@d{32} Does that mean…@d{96} you won?!\n@c{smile_ehalf}I should have known you had it in you.\n$@c{smile_eclosed}Of course… I always had that feeling.\n@c{smile}It's over now, right? You ended the loop.\n$@c{smile_ehalf}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$I'll be the only one to remember what you did.\n@c{angry_mopen}I'll try not to forget!\n$@c{smile_wave_wink}Just kidding!@d{64} @c{smile}I'd never forget.@d{32}\nYour legend will live on in our hearts.\n$@c{smile_wave}Anyway,@d{64} it's getting late…@d{96} I think?\nIt's hard to tell in this place.\n$Let's go home. @c{smile_wave_wink}Maybe tomorrow, we can have another battle, for old time's sake?",
"ending_female": "@c{smile}Oh? You won?@d{96} @c{smile_eclosed}I guess I should've known.\nBut, you're back now.\n$@c{smile}It's over.@d{64} You ended the loop.\n$@c{serious_smile_fists}You fulfilled your dream too, didn't you?\nYou didn't lose even once.\n$@c{neutral}I'm the only one who'll remember what you did.@d{96}\nI guess that's okay, isn't it?\n$@c{serious_smile_fists}Your legend will always live on in our hearts.\n$@c{smile_eclosed}Anyway, I've had about enough of this place, haven't you? Let's head home.\n$@c{serious_smile_fists}Maybe when we get back, we can have another battle?\nIf you're up to it.",
"ending_endless": "Congratulations on reaching the current end!\nMore content is coming soon.",
"ending_name": "Devs"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"pp": "PP", "pp": "PP",
"power": "いりょく", "power": "威力",
"accuracy": "めいちゅう", "accuracy": "命中",
"abilityFlyInText": " {{pokemonName}}の {{passive}}{{abilityName}}", "abilityFlyInText": " {{pokemonName}}の\n{{passive}}{{abilityName}}",
"passive": "Passive " "passive": "パッシブ "
} }

View File

@ -32,7 +32,7 @@
"noPokerus": "ポケルス - なし", "noPokerus": "ポケルス - なし",
"sortByNumber": "No.", "sortByNumber": "No.",
"sortByCost": "ポイント", "sortByCost": "ポイント",
"sortByCandies": "の数", "sortByCandies": "アメの数",
"sortByIVs": "個体値", "sortByIVs": "個体値",
"sortByName": "名前" "sortByName": "名前"
} }

View File

@ -12,26 +12,26 @@
"dailyRunAttempts": "デイリーラン", "dailyRunAttempts": "デイリーラン",
"dailyRunWins": "デイリーラン勝利", "dailyRunWins": "デイリーラン勝利",
"endlessRuns": "エンドレスラン", "endlessRuns": "エンドレスラン",
"highestWaveEndless": "エンドレス最高", "highestWaveEndless": "エンドレス最高ラウンド",
"highestMoney": "最大貯金", "highestMoney": "最大貯金",
"highestDamage": "最大ダメージ", "highestDamage": "最大ダメージ",
"highestHPHealed": "最大HP回復", "highestHPHealed": "最大HP回復",
"pokemonEncountered": "遭遇したポケモン", "pokemonEncountered": "遭遇したポケモン",
"pokemonDefeated": "倒したポケモン", "pokemonDefeated": "倒したポケモン",
"pokemonCaught": "捕まえたポケモン", "pokemonCaught": "捕まえたポケモン",
"eggsHatched": "孵化したタマゴ", "eggsHatched": "ふかしたタマゴ",
"subLegendsSeen": "見つけた順伝説ポケモン", "subLegendsSeen": "見つけた順伝説ポケモン",
"subLegendsCaught": "捕まえた準伝説ポケモン", "subLegendsCaught": "捕まえた準伝説ポケモン",
"subLegendsHatched": "孵化した準伝説ポケモン", "subLegendsHatched": "ふかした準伝説ポケモン",
"legendsSeen": "見つけた伝説ポケモン", "legendsSeen": "見つけた伝説ポケモン",
"legendsCaught": "捕まえた伝説ポケモン", "legendsCaught": "捕まえた伝説ポケモン",
"legendsHatched": "孵化した伝説ポケモン", "legendsHatched": "ふかした伝説ポケモン",
"mythicalsSeen": "見つけた幻ポケモン", "mythicalsSeen": "見つけた幻ポケモン",
"mythicalsCaught": "捕まえた幻ポケモン", "mythicalsCaught": "捕まえた幻ポケモン",
"mythicalsHatched": "孵化した幻ポケモン", "mythicalsHatched": "ふかした幻ポケモン",
"shiniesSeen": "見つけた色違いポケモン", "shiniesSeen": "見つけた色違いポケモン",
"shiniesCaught": "捕まえた色違いポケモン", "shiniesCaught": "捕まえた色違いポケモン",
"shiniesHatched": "孵化した色違いポケモン", "shiniesHatched": "ふかした色違いポケモン",
"pokemonFused": "吸収合体したポケモン", "pokemonFused": "吸収合体したポケモン",
"trainersDefeated": "倒したトレーナー", "trainersDefeated": "倒したトレーナー",
"eggsPulled": "引いたタマゴ", "eggsPulled": "引いたタマゴ",

View File

@ -1,8 +1,8 @@
{ {
"Erratic": "60まんタイプ", "Erratic": "60タイプ",
"Fast": "80まんタイプ", "Fast": "80タイプ",
"Medium_Fast": "100まんタイプ", "Medium_Fast": "100タイプ",
"Medium_Slow": "105まんタイプ", "Medium_Slow": "105タイプ",
"Slow": "125まんタイプ", "Slow": "125タイプ",
"Fluctuating": "164まんタイプ" "Fluctuating": "164タイプ"
} }

View File

@ -24,6 +24,6 @@
"linkGoogle": "Google連携", "linkGoogle": "Google連携",
"unlinkGoogle": "Google連携解除", "unlinkGoogle": "Google連携解除",
"cancel": "キャンセル", "cancel": "キャンセル",
"losingProgressionWarning": "戦闘開始からの データが 保存されません。\nよろしいですか", "losingProgressionWarning": "戦闘開始からの データが セーブされません。\nよろしいですか",
"noEggs": "現在は タマゴを 孵化していません!" "noEggs": "現在は タマゴを ふかしていません!"
} }

View File

@ -353,7 +353,7 @@
"description": "やせいのポケモンがかくれとくせいをもつかくりつをおおきくふやす" "description": "やせいのポケモンがかくれとくせいをもつかくりつをおおきくふやす"
}, },
"IV_SCANNER": { "IV_SCANNER": {
"name": "こたいち たんちき", "name": "こたいちスキャナー",
"description": "やせいのポケモンのこたいちをスキャンできる。スタックごとに2つのこたいちがあきらかになる。もっともたかいこたいちがさいしょにひょうじされる" "description": "やせいのポケモンのこたいちをスキャンできる。スタックごとに2つのこたいちがあきらかになる。もっともたかいこたいちがさいしょにひょうじされる"
}, },
"DNA_SPLICERS": { "DNA_SPLICERS": {

View File

@ -1,12 +1,12 @@
{ {
"surviveDamageApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で もちこたえた!", "surviveDamageApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で もちこたえた!",
"turnHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!", "turnHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 体力を 回復した",
"hitHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 回復!", "hitHealApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 少し 体力を 回復した",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 復活した!", "pokemonInstantReviveApply": "{{pokemonNameWithAffix}}は\n{{typeName}}で 復活した!",
"resetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った", "resetNegativeStatStageApply": "{{pokemonNameWithAffix}}は {{typeName}}で\n下がった能力が 元に戻った",
"moneyInterestApply": "{{typeName}}から {{moneyAmount}}円 取得した!", "moneyInterestApply": "{{typeName}}から {{moneyAmount}}円 取得した!",
"turnHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 吸い取った!", "turnHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 吸い取った!",
"contactHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を うばい取った!", "contactHeldItemTransferApply": "{{pokemonName}}の {{typeName}}が\n{{pokemonNameWithAffix}}の {{itemName}}を 奪い取った!",
"enemyTurnHealApply": "{{pokemonNameWithAffix}}は\n体力を 回復", "enemyTurnHealApply": "{{pokemonNameWithAffix}}は\n体力を 回復",
"bypassSpeedChanceApply": "{{pokemonName}}は {{itemName}}で\n行動が はやくなった" "bypassSpeedChanceApply": "{{pokemonName}}は {{itemName}}で\n行動が はやくなった"
} }

View File

@ -1,64 +1,69 @@
{ {
"hitWithRecoil": "{{pokemonName}}は\nはんどうによる ダメージを うけた", "hitWithRecoil": "{{pokemonName}}は\n反動による ダメージを 受けた",
"cutHpPowerUpMove": "{{pokemonName}}は\nたいりょくを けずって パワーぜんかい", "cutHpPowerUpMove": "{{pokemonName}}は\n体力を 削って 技の 威力を 上がった",
"absorbedElectricity": "{{pokemonName}}は\n でんきを きゅうしゅうした", "absorbedElectricity": "{{pokemonName}}は\n 電気を 吸収した",
"switchedStatChanges": "{{pokemonName}}は あいてと じぶんの\nのうりょくへんかを いれかえた", "switchedStatChanges": "{{pokemonName}}は 相手と 自分の\n能力変化を 入れ替えた",
"sharedGuard": "{{pokemonName}}は\nおたがいのガードを シェアした", "switchedTwoStatChanges": "{{pokemonName}}は 相手と 自分の {{firstStat}}と\n{{secondStat}}の 能力変化を 入れ替えた!",
"sharedPower": "{{pokemonName}}は\nおたがいのパワーを シェアした", "switchedStat": "{{pokemonName}}は 相手と {{stat}}を 入れ替えた!",
"goingAllOutForAttack": "{{pokemonName}}は\nほんきを だした", "sharedGuard": "{{pokemonName}}は\nお互いのガードを シェアした",
"regainedHealth": "{{pokemonName}}は\nたいりょくを かいふくした", "sharedPower": "{{pokemonName}}は\nお互いのパワーを シェアした",
"keptGoingAndCrashed": "いきおいあまって {{pokemonName}}は\nじめんに ぶつかった", "goingAllOutForAttack": "{{pokemonName}}は\n本気を 出した",
"fled": "{{pokemonName}}は にげだした!", "regainedHealth": "{{pokemonName}}は\n体力を 回復した",
"cannotBeSwitchedOut": "{{pokemonName}}を\nもどすことが できない", "keptGoingAndCrashed": "勢い余って {{pokemonName}}は\n地面に ぶつかった",
"swappedAbilitiesWithTarget": "{{pokemonName}}は\nおたがいの とくせいを いれかえた", "fled": "{{pokemonName}}は 逃げ出した!",
"coinsScatteredEverywhere": "こばんが あたりに ちらばった!", "cannotBeSwitchedOut": "{{pokemonName}}を\n戻すことが できない",
"swappedAbilitiesWithTarget": "{{pokemonName}}は\nお互いの 特性を 入れ替えた",
"coinsScatteredEverywhere": "小判が 辺りに 散らばった!",
"attackedByItem": "{{pokemonName}}に\n{{itemName}}が おそいかかる!", "attackedByItem": "{{pokemonName}}に\n{{itemName}}が おそいかかる!",
"whippedUpAWhirlwind": "{{pokemonName}}の まわりで\nくうきが うずをまく", "whippedUpAWhirlwind": "{{pokemonName}}の 周りで\n空気が 渦を巻く",
"flewUpHigh": "{{pokemonName}}は\nそらたかく とびあがった", "flewUpHigh": "{{pokemonName}}は\n空高く 飛び上がった",
"tookInSunlight": "{{pokemonName}}は\nひかりを きゅうしゅうした!", "tookInSunlight": "{{pokemonName}}は\n光を 吸収した!",
"dugAHole": "{{pokemonName}}は\nじめんに もぐった", "dugAHole": "{{pokemonName}}は\n地面に 潜った",
"loweredItsHead": "{{pokemonName}}は\nくびを ひっこめた", "loweredItsHead": "{{pokemonName}}は\n首を 引っ込めた",
"isGlowing": "{{pokemonName}}を\nはげしいひかりが つつむ", "isGlowing": "{{pokemonName}}を\n激しい光が 包む",
"bellChimed": "すずのおとが ひびきわたった!", "bellChimed": "鈴の音が 響き渡った!",
"foresawAnAttack": "{{pokemonName}}は\nみらいに こうげきを よちした", "foresawAnAttack": "{{pokemonName}}は\n未来に 攻撃を 予知した",
"hidUnderwater": "{{pokemonName}}は\nすいちゅうに みをひそめた", "isTighteningFocus": "{{pokemonName}}は\n集中力を 高めている",
"soothingAromaWaftedThroughArea": "ここちよい かおりが ひろがった!", "hidUnderwater": "{{pokemonName}}は\n水中に 身を潜めた",
"sprangUp": "{{pokemonName}}は\nたかく とびはねた", "soothingAromaWaftedThroughArea": "心地よい 香りが 広がった!",
"choseDoomDesireAsDestiny": "{{pokemonName}}は\nはめつのねがいを みらいに たくした", "sprangUp": "{{pokemonName}}は\n高く 飛び跳ねた",
"vanishedInstantly": "{{pokemonName}}の すがたが\nいっしゅんにして きえた", "choseDoomDesireAsDestiny": "{{pokemonName}}は\nはめつのねがいを 未来に 託した",
"tookTargetIntoSky": "{{pokemonName}}は {{targetName}}を\nじょうくうに つれさった", "vanishedInstantly": "{{pokemonName}}の 姿が\n一瞬にして 消えた",
"becameCloakedInFreezingLight": "{{pokemonName}}は\nつめたいひかりに つつまれた", "tookTargetIntoSky": "{{pokemonName}}は {{targetName}}を\n上空に 連れ去った",
"becameCloakedInFreezingAir": "{{pokemonName}}は\nこごえるくうきに つつまれた", "becameCloakedInFreezingLight": "{{pokemonName}}は\n冷たい光に 包まれた",
"isChargingPower": "{{pokemonName}}は\nパワーを ためこんでいる", "becameCloakedInFreezingAir": "{{pokemonName}}は\n凍える空気に 包まれた",
"burnedItselfOut": "{{pokemonName}}の ほのうは\nもえつきた", "isChargingPower": "{{pokemonName}}は\nパワーを 溜め込んでいる",
"startedHeatingUpBeak": "{{pokemonName}}は\nクチバシを かねつしはじめた", "burnedItselfOut": "{{pokemonName}}の 炎は 燃え尽きた!",
"startedHeatingUpBeak": "{{pokemonName}}は\nクチバシを 加熱し始めた",
"setUpShellTrap": "{{pokemonName}}は\nトラップシェルを 仕掛けた", "setUpShellTrap": "{{pokemonName}}は\nトラップシェルを 仕掛けた",
"isOverflowingWithSpacePower": "{{pokemonName}}に\nうちゅうの ちからが あふれだす!", "isOverflowingWithSpacePower": "{{pokemonName}}に\n宇宙の 力が 溢れ出す!",
"usedUpAllElectricity": "{{pokemonName}}は\nでんきを つかいきった!", "usedUpAllElectricity": "{{pokemonName}}は\n電気を 使い切った!",
"stoleItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を ぬすんだ!", "stoleItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を んだ!",
"incineratedItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を やした!", "incineratedItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を やした!",
"knockedOffItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を はたきとした!", "knockedOffItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を はたきとした!",
"tookMoveAttack": "{{pokemonName}}は\n{{moveName}}の こうげきを うけた!", "tookMoveAttack": "{{pokemonName}}は\n{{moveName}}の 攻撃を 受けた!",
"cutOwnHpAndMaximizedStat": "{{pokemonName}}は\nたいりょくを けずって {{statName}}ぜんかい", "cutOwnHpAndMaximizedStat": "{{pokemonName}}は\n体力を 削って {{statName}}全開",
"copiedStatChanges": "{{pokemonName}}は {{targetName}}の\nのうりょくへんかを コピーした!", "copiedStatChanges": "{{pokemonName}}は {{targetName}}の\n能力変化を コピーした!",
"magnitudeMessage": "マグニチュード{{magnitude}}", "magnitudeMessage": "マグニチュード{{magnitude}}",
"tookAimAtTarget": "{{pokemonName}}は {{targetName}}に\nねらいを さだめた!", "tookAimAtTarget": "{{pokemonName}}は {{targetName}}に\n狙いを 定めた!",
"transformedIntoType": "{{pokemonName}}は\n{{typeName}}タイプに なった!", "transformedIntoType": "{{pokemonName}}は\n{{typeName}}タイプに なった!",
"copiedMove": "{{pokemonName}}は\n{{moveName}}を コピーした!", "copiedMove": "{{pokemonName}}は\n{{moveName}}を コピーした!",
"sketchedMove": "{{pokemonName}}は\n{{moveName}}を スケッチした!", "sketchedMove": "{{pokemonName}}は\n{{moveName}}を スケッチした!",
"acquiredAbility": "{{pokemonName}}の とくせいが\n{{abilityName}}に なった!", "acquiredAbility": "{{pokemonName}}の 特性が\n{{abilityName}}に なった!",
"copiedTargetAbility": "{{pokemonName}}は\n{{targetName}}の {{abilityName}}を コピーした!", "copiedTargetAbility": "{{pokemonName}}は\n{{targetName}}の {{abilityName}}を コピーした!",
"transformedIntoTarget": "{{pokemonName}}は\n{{targetName}}に へんしんした!", "transformedIntoTarget": "{{pokemonName}}は\n{{targetName}}に 変身した!",
"tryingToTakeFoeDown": "{{pokemonName}}は あいてを\nみちづれに しようとしている", "tryingToTakeFoeDown": "{{pokemonName}}は 相手を\nみちづれに しようとしている",
"addType": "{{pokemonName}}に\n{{typeName}}タイプが ついかされた!", "addType": "{{pokemonName}}に\n{{typeName}}タイプが 追加された!",
"cannotUseMove": "{{pokemonName}}は\n{{moveName}}を つかえなかった!", "cannotUseMove": "{{pokemonName}}は\n{{moveName}}を 使えなかった!",
"healHp": "{{pokemonName}}の\nたいりょくが かいふくした", "healHp": "{{pokemonName}}の\n体力が 回復した",
"sacrificialFullRestore": "{{pokemonName}}の\nねがいごとが かなった", "sacrificialFullRestore": "{{pokemonName}}の\nいやしのねがいが 叶った",
"invertStats": "{{pokemonName}}の\nのうりょくへんかが ぎゃくてんした", "invertStats": "{{pokemonName}}は\n能力変化が ひっくり返った",
"resetStats": "{{pokemonName}}の\nのうりょくへんかが もとにもどった", "resetStats": "{{pokemonName}}の\n能力変化が 元に戻った",
"faintCountdown": "{{pokemonName}}は\n{{turnCount}}ターンごに ほろびてしまう!", "statEliminated": "全ての 能力変化が 元に戻った!",
"faintCountdown": "{{pokemonName}}は\n{{turnCount}}ターン後に 滅びてしまう!",
"copyType": "{{pokemonName}}は {{targetPokemonName}}と\n同じタイプに なった", "copyType": "{{pokemonName}}は {{targetPokemonName}}と\n同じタイプに なった",
"suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!", "suppressAbilities": "{{pokemonName}}の 特性が 効かなくなった!",
"revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった", "revivalBlessing": "{{pokemonName}}は\n復活して 戦えるようになった",
"swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた" "swapArenaTags": "{{pokemonName}}は\nお互いの 場の 効果を 入れ替えた",
"exposedMove": "{{pokemonName}}は {{targetPokemonName}}の\n正体を 見破った"
} }

View File

@ -1,8 +1,8 @@
{ {
"SEND_OUT": "いれかえる", "SEND_OUT": "入れ替える",
"SUMMARY": "つよさをみる", "SUMMARY": "強さを見る",
"CANCEL": "やめる", "CANCEL": "やめる",
"RELEASE": "がす", "RELEASE": "がす",
"APPLY": "つかう", "APPLY": "使う",
"TEACH": "おしえる" "TEACH": "える"
} }

View File

@ -1,7 +1,7 @@
{ {
"moveset": "わざ", "moveset": "",
"gender": "せいべつ:", "gender": "性別:",
"ability": "とくせい:", "ability": "特性:",
"nature": "せいかく:", "nature": "性格:",
"form": "すがた:" "form": "姿:"
} }

View File

@ -1 +1,44 @@
{} {
"pokemonInfo": "ポケモン情報",
"status": "ステータス",
"powerAccuracyCategory": "威力\n命中\n分類",
"type": "タイプ",
"unknownTrainer": "",
"ot": "親",
"nature": "性格",
"expPoints": "経験値",
"nextLv": "次のレベルまで",
"cancel": "キャンセル",
"memoString": "{{natureFragment}}な性格。\n{{metFragment}}",
"metFragment": {
"normal": "{{biome}}で\nLv.{{level}}の時に出会った。",
"apparently": "{{biome}}で\nLv.{{level}}の時に出会ったようだ。"
},
"natureFragment": {
"Hardy": "{{nature}}",
"Lonely": "{{nature}}",
"Brave": "{{nature}}",
"Adamant": "{{nature}}",
"Naughty": "{{nature}}",
"Bold": "{{nature}}",
"Docile": "{{nature}}",
"Relaxed": "{{nature}}",
"Impish": "{{nature}}",
"Lax": "{{nature}}",
"Timid": "{{nature}}",
"Hasty": "{{nature}}",
"Serious": "{{nature}}",
"Jolly": "{{nature}}",
"Naive": "{{nature}}",
"Modest": "{{nature}}",
"Mild": "{{nature}}",
"Quiet": "{{nature}}",
"Bashful": "{{nature}}",
"Rash": "{{nature}}",
"Calm": "{{nature}}",
"Gentle": "{{nature}}",
"Sassy": "{{nature}}",
"Careful": "{{nature}}",
"Quirky": "{{nature}}"
}
}

View File

@ -28,10 +28,10 @@
"SPDshortened": "速さ", "SPDshortened": "速さ",
"runInfo": "ラン情報", "runInfo": "ラン情報",
"money": "お金", "money": "お金",
"runLength": "ラン最高ウェーブ", "runLength": "時間",
"viewHeldItems": "手持ちアイテム", "viewHeldItems": "持たせたアイテム",
"hallofFameText": "殿堂へようこそ", "hallofFameText": "殿堂入り おめでとう",
"hallofFameText_female": "殿堂へようこそ", "hallofFameText_female": "殿堂入り おめでとう",
"viewHallOfFame": "殿堂登録を見る!", "viewHallOfFame": "殿堂登録を見る!",
"viewEndingSplash": "クリア後のアートを見る!" "viewEndingSplash": "クリア後のアートを見る!"
} }

View File

@ -1,7 +1,7 @@
{ {
"overwriteData": "選択した スロットに データを 上書きします?", "overwriteData": "選択した スロットに データを 上書きします?",
"loading": "読込中…", "loading": "読込中…",
"wave": "", "wave": "ラウンド",
"lv": "Lv", "lv": "Lv",
"empty": "なし" "empty": "なし"
} }

View File

@ -1 +1,36 @@
{} {
"battlesWon": "Battles Won!",
"joinTheDiscord": "Join the Discord!",
"infiniteLevels": "Infinite Levels!",
"everythingStacks": "Everything Stacks!",
"optionalSaveScumming": "Optional Save Scumming!",
"biomes": "35 Biomes!",
"openSource": "Open Source!",
"playWithSpeed": "Play with 5x Speed!",
"liveBugTesting": "Live Bug Testing!",
"heavyInfluence": "Heavy RoR2 Influence!",
"pokemonRiskAndPokemonRain": "Pokémon Risk and Pokémon Rain!",
"nowWithMoreSalt": "Now with 33% More Salt!",
"infiniteFusionAtHome": "Infinite Fusion at Home!",
"brokenEggMoves": "Broken Egg Moves!",
"magnificent": "Magnificent!",
"mubstitute": "Mubstitute!",
"thatsCrazy": "That's Crazy!",
"oranceJuice": "Orance Juice!",
"questionableBalancing": "Questionable Balancing!",
"coolShaders": "Cool Shaders!",
"aiFree": "AI-Free!",
"suddenDifficultySpikes": "Sudden Difficulty Spikes!",
"basedOnAnUnfinishedFlashGame": "Based on an Unfinished Flash Game!",
"moreAddictiveThanIntended": "More Addictive than Intended!",
"mostlyConsistentSeeds": "Mostly Consistent Seeds!",
"achievementPointsDontDoAnything": "Achievement Points Don't Do Anything!",
"youDoNotStartAtLevel": "You Do Not Start at Level 2000!",
"dontTalkAboutTheManaphyEggIncident": "Don't Talk About the Manaphy Egg Incident!",
"alsoTryPokengine": "Also Try Pokéngine!",
"alsoTryEmeraldRogue": "Also Try Emerald Rogue!",
"alsoTryRadicalRed": "Also Try Radical Red!",
"eeveeExpo": "Eevee Expo!",
"ynoproject": "YNOproject!",
"breedersInSpace": "Breeders in space!"
}

View File

@ -16,17 +16,17 @@
"snowStartMessage": "雪が 降り始めた!", "snowStartMessage": "雪が 降り始めた!",
"snowLapseMessage": "雪が 降っている!", "snowLapseMessage": "雪が 降っている!",
"snowClearMessage": "雪が 止んだ!", "snowClearMessage": "雪が 止んだ!",
"fogStartMessage": "足下に 霧(きり)が立ち込めた!", "fogStartMessage": "足下に 霧 立ち込めた!",
"fogLapseMessage": "足下に 霧(きり)が 立ち込めている!", "fogLapseMessage": "足下に 霧が 立ち込めている!",
"fogClearMessage": "足下の 霧(きり)が消え去った!", "fogClearMessage": "足下の 霧 消え去った!",
"heavyRainStartMessage": "強い雨が 降り始めた!", "heavyRainStartMessage": "強い雨が 降り始めた!",
"heavyRainLapseMessage": "強い雨が 降っている!", "heavyRainLapseMessage": "強い雨が 降っている!",
"heavyRainClearMessage": "強い雨が あがった!", "heavyRainClearMessage": "強い雨が あがった!",
"harshSunStartMessage": "日差しが とても強くなった!", "harshSunStartMessage": "日差しが とても強くなった!",
"harshSunLapseMessage": "日差しが とても強い!", "harshSunLapseMessage": "日差しが とても強い!",
"harshSunClearMessage": "日差しが 元に戻った!", "harshSunClearMessage": "日差しが 元に戻った!",
"strongWindsStartMessage": "謎(なぞ)の 乱気流(らんきりゅう)が\nひこうポケモンを (まも)る!", "strongWindsStartMessage": "謎の 乱気流が\nひこうポケモンを 護る",
"strongWindsLapseMessage": "謎(なぞ)の 乱気流(らんきりゅう)の 勢(いきお)いは 止まらない!", "strongWindsLapseMessage": "謎の 乱気流の 勢いは 止まらない!",
"strongWindsEffectMessage": "謎(なぞ)の 乱気流(らんきりゅう)が 攻撃(こうげき)を 弱(よわ)めた!", "strongWindsEffectMessage": "謎の 乱気流が 攻撃を 弱めた!",
"strongWindsClearMessage": "謎(なぞ)の 乱気流(らんきりゅう)が おさまった!" "strongWindsClearMessage": "謎の 乱気流が おさまった!"
} }

View File

@ -36,5 +36,6 @@
"matBlock": "마룻바닥세워막기", "matBlock": "마룻바닥세워막기",
"craftyShield": "트릭가드", "craftyShield": "트릭가드",
"tailwind": "순풍", "tailwind": "순풍",
"happyHour": "해피타임" "happyHour": "해피타임",
} "safeguard": "신비의부적"
}

View File

@ -36,5 +36,6 @@
"matBlock": "Mat Block", "matBlock": "Mat Block",
"craftyShield": "Crafty Shield", "craftyShield": "Crafty Shield",
"tailwind": "Tailwind", "tailwind": "Tailwind",
"happyHour": "Happy Hour" "happyHour": "Happy Hour",
"safeguard": "Safeguard"
} }

View File

@ -36,5 +36,6 @@
"matBlock": "掀榻榻米", "matBlock": "掀榻榻米",
"craftyShield": "戏法防守", "craftyShield": "戏法防守",
"tailwind": "顺风", "tailwind": "顺风",
"happyHour": "快乐时光" "happyHour": "快乐时光",
} "safeguard": "神秘守护"
}

View File

@ -2488,7 +2488,7 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier {
} }
getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierTypes.ModifierType): string { getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierTypes.ModifierType): string {
return i18next.t("modifier:turnHeldItemTransferApply", { pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), itemName: item.name, pokemonName: pokemon.name, typeName: this.type.name }); return i18next.t("modifier:turnHeldItemTransferApply", { pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), itemName: item.name, pokemonName: pokemon.getNameToRender(), typeName: this.type.name });
} }
getMaxHeldItemCount(pokemon: Pokemon): integer { getMaxHeldItemCount(pokemon: Pokemon): integer {

View File

@ -1,31 +1,34 @@
import BattleScene from "#app/battle-scene.js"; import BattleScene from "#app/battle-scene";
import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability.js"; import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability";
import { Stat } from "#app/enums/stat.js"; import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect.js"; import { StatusEffect } from "#app/enums/status-effect";
import Pokemon from "#app/field/pokemon.js"; import Pokemon, { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
import i18next from "i18next"; import i18next from "i18next";
import * as Utils from "#app/utils.js"; import * as Utils from "#app/utils";
import { BattleEndPhase } from "./battle-end-phase"; import { BattleEndPhase } from "./battle-end-phase";
import { NewBattlePhase } from "./new-battle-phase"; import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
export class AttemptRunPhase extends PokemonPhase { export class AttemptRunPhase extends PokemonPhase {
constructor(scene: BattleScene, fieldIndex: integer) { constructor(scene: BattleScene, fieldIndex: number) {
super(scene, fieldIndex); super(scene, fieldIndex);
} }
start() { start() {
super.start(); super.start();
const playerPokemon = this.getPokemon(); const playerField = this.scene.getPlayerField();
const enemyField = this.scene.getEnemyField(); 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); 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.playSound("se/flee");
this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
@ -53,4 +56,48 @@ export class AttemptRunPhase extends PokemonPhase {
this.end(); 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);
}
} }

View File

@ -107,8 +107,9 @@ export class CommandPhase extends FieldPhase {
// Decides between a Disabled, Not Implemented, or No PP translation message // Decides between a Disabled, Not Implemented, or No PP translation message
const errorMessage = const errorMessage =
playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" : playerPokemon.isMoveRestricted(move.moveId)
move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; ? 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 const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => { this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {

View File

@ -377,16 +377,16 @@ export class MoveEffectPhase extends PokemonPhase {
return false; return false;
} }
const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user!, target); // TODO: is the bang correct here? const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user, target);
if (moveAccuracy === -1) { if (moveAccuracy === -1) {
return true; return true;
} }
const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove()); const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove());
const rand = user.randSeedInt(100, 1); const rand = user.randSeedInt(100);
return rand <= moveAccuracy * (accuracyMultiplier!); // TODO: is this bang correct? return rand < (moveAccuracy * accuracyMultiplier);
} }
/** Returns the {@linkcode Pokemon} using this phase's invoked move */ /** Returns the {@linkcode Pokemon} using this phase's invoked move */

View File

@ -44,8 +44,8 @@ export class MovePhase extends BattlePhase {
this.cancelled = false; this.cancelled = false;
} }
canMove(): boolean { canMove(ignoreDisableTags?: boolean): boolean {
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length; 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 */ /**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]); console.log(Moves[this.move.moveId]);
if (!this.canMove()) { if (!this.canMove(true)) {
if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) {
this.scene.queueMessage(i18next.t("battle:moveDisabled", { moveName: this.move.getName() }));
}
if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails 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.fail();
this.showMoveText(); this.showMoveText();

View File

@ -1,9 +1,7 @@
import BattleScene from "#app/battle-scene.js"; import BattleScene from "#app/battle-scene.js";
import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/ability.js"; import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/ability.js";
import { BattlerTagLapseType } from "#app/data/battler-tags.js"; import { BattlerTagLapseType } from "#app/data/battler-tags.js";
import { allMoves } from "#app/data/move.js";
import { TerrainType } from "#app/data/terrain.js"; import { TerrainType } from "#app/data/terrain.js";
import { Moves } from "#app/enums/moves.js";
import { WeatherType } from "#app/enums/weather-type.js"; import { WeatherType } from "#app/enums/weather-type.js";
import { TurnEndEvent } from "#app/events/battle-scene.js"; import { TurnEndEvent } from "#app/events/battle-scene.js";
import Pokemon from "#app/field/pokemon.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 { TurnHealModifier, EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier, TurnStatusEffectModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
import i18next from "i18next"; import i18next from "i18next";
import { FieldPhase } from "./field-phase"; import { FieldPhase } from "./field-phase";
import { MessagePhase } from "./message-phase";
import { PokemonHealPhase } from "./pokemon-heal-phase"; import { PokemonHealPhase } from "./pokemon-heal-phase";
export class TurnEndPhase extends FieldPhase { export class TurnEndPhase extends FieldPhase {
@ -28,11 +25,6 @@ export class TurnEndPhase extends FieldPhase {
const handlePokemon = (pokemon: Pokemon) => { const handlePokemon = (pokemon: Pokemon) => {
pokemon.lapseTags(BattlerTagLapseType.TURN_END); 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); this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon);
if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) {

View File

@ -127,8 +127,6 @@ export default class PokemonData {
this.summonData.stats = source.summonData.stats; this.summonData.stats = source.summonData.stats;
this.summonData.statStages = source.summonData.statStages; this.summonData.statStages = source.summonData.statStages;
this.summonData.moveQueue = source.summonData.moveQueue; 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.abilitySuppressed = source.summonData.abilitySuppressed;
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied; this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;

View File

@ -2,12 +2,6 @@ import { Stat } from "#enums/stat";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { BattlerTagType } from "#app/enums/battler-tag-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 { toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -15,7 +9,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils"; import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser"; 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; const TIMEOUT = 20 * 1000;
@ -39,36 +33,31 @@ describe("Abilities - Parental Bond", () => {
game.override.disableCrits(); game.override.disableCrits();
game.override.ability(Abilities.PARENTAL_BOND); game.override.ability(Abilities.PARENTAL_BOND);
game.override.enemySpecies(Species.SNORLAX); game.override.enemySpecies(Species.SNORLAX);
game.override.enemyAbility(Abilities.INSOMNIA); game.override.enemyAbility(Abilities.FUR_COAT);
game.override.enemyMoveset(SPLASH_ONLY); game.override.enemyMoveset(SPLASH_ONLY);
game.override.startingLevel(100); game.override.startingLevel(100);
game.override.enemyLevel(100); game.override.enemyLevel(100);
}); });
test( it(
"ability should add second strike to attack move", "should add second strike to attack move",
async () => { async () => {
game.override.moveset([Moves.TACKLE]); game.override.moveset([Moves.TACKLE]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
let enemyStartingHp = enemyPokemon.hp; let enemyStartingHp = enemyPokemon.hp;
game.move.select(Moves.TACKLE); 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; const firstStrikeDamage = enemyStartingHp - enemyPokemon.hp;
enemyStartingHp = enemyPokemon.hp; enemyStartingHp = enemyPokemon.hp;
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
const secondStrikeDamage = enemyStartingHp - enemyPokemon.hp; const secondStrikeDamage = enemyStartingHp - enemyPokemon.hp;
@ -77,556 +66,460 @@ describe("Abilities - Parental Bond", () => {
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should apply secondary effects to both strikes", "should apply secondary effects to both strikes",
async () => { async () => {
game.override.moveset([Moves.POWER_UP_PUNCH]); game.override.moveset([Moves.POWER_UP_PUNCH]);
game.override.enemySpecies(Species.AMOONGUSS); game.override.enemySpecies(Species.AMOONGUSS);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); 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.turnData.hitCount).toBe(2);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not apply to Status moves", "should not apply to Status moves",
async () => { async () => {
game.override.moveset([Moves.BABY_DOLL_EYES]); game.override.moveset([Moves.BABY_DOLL_EYES]);
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()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
game.move.select(Moves.BABY_DOLL_EYES); 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); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not apply to multi-hit moves", "should not apply to multi-hit moves",
async () => { async () => {
game.override.moveset([Moves.DOUBLE_HIT]); game.override.moveset([Moves.DOUBLE_HIT]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.DOUBLE_HIT);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not apply to self-sacrifice moves", "should not apply to self-sacrifice moves",
async () => { async () => {
game.override.moveset([Moves.SELF_DESTRUCT]); game.override.moveset([Moves.SELF_DESTRUCT]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.SELF_DESTRUCT);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to("DamagePhase", false);
expect(leadPokemon.turnData.hitCount).toBe(1); expect(leadPokemon.turnData.hitCount).toBe(1);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not apply to Rollout", "should not apply to Rollout",
async () => { async () => {
game.override.moveset([Moves.ROLLOUT]); game.override.moveset([Moves.ROLLOUT]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.ROLLOUT);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to("DamagePhase", false);
expect(leadPokemon.turnData.hitCount).toBe(1); expect(leadPokemon.turnData.hitCount).toBe(1);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not apply multiplier to fixed-damage moves", "should not apply multiplier to fixed-damage moves",
async () => { async () => {
game.override.moveset([Moves.DRAGON_RAGE]); game.override.moveset([Moves.DRAGON_RAGE]);
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()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
const enemyStartingHp = enemyPokemon.hp;
game.move.select(Moves.DRAGON_RAGE); 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 }, TIMEOUT
); );
test( it(
"ability should not apply multiplier to counter moves", "should not apply multiplier to counter moves",
async () => { async () => {
game.override.moveset([Moves.COUNTER]); 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()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
const playerStartingHp = leadPokemon.hp;
const enemyStartingHp = enemyPokemon.hp;
game.move.select(Moves.COUNTER); 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 }, TIMEOUT
); );
test( it(
"ability should not apply to multi-target moves", "should not apply to multi-target moves",
async () => { async () => {
game.override.battleType("double"); game.override.battleType("double");
game.override.moveset([Moves.EARTHQUAKE]); 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(); 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); game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.EARTHQUAKE, 1); 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)); playerPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should apply to multi-target moves when hitting only one target", "should apply to multi-target moves when hitting only one target",
async () => { async () => {
game.override.moveset([Moves.EARTHQUAKE]); game.override.moveset([Moves.EARTHQUAKE]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to("DamagePhase", false);
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should only trigger post-target move effects once", "should only trigger post-target move effects once",
async () => { async () => {
game.override.moveset([Moves.MIND_BLOWN]); game.override.moveset([Moves.MIND_BLOWN]);
await game.startBattle([Species.PIDGEOT]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.MIND_BLOWN);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to("DamagePhase", false);
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
// This test will time out if the user faints // 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 }, TIMEOUT
); );
test( it(
"Burn Up only removes type after second strike with this ability", "Burn Up only removes type after the second strike",
async () => { async () => {
game.override.moveset([Moves.BURN_UP]); game.override.moveset([Moves.BURN_UP]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
game.move.select(Moves.BURN_UP); game.move.select(Moves.BURN_UP);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("MoveEffectPhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.hp).toBeGreaterThan(0); expect(enemyPokemon.hp).toBeGreaterThan(0);
expect(leadPokemon.isOfType(Type.FIRE)).toBe(true); 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); expect(leadPokemon.isOfType(Type.FIRE)).toBe(false);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"Moves boosted by this ability and Multi-Lens should strike 4 times", "Moves boosted by this ability and Multi-Lens should strike 4 times",
async () => { async () => {
game.override.moveset([Moves.TACKLE]); game.override.moveset([Moves.TACKLE]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(4); expect(leadPokemon.turnData.hitCount).toBe(4);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"Super Fang boosted by this ability and Multi-Lens should strike twice", "Super Fang boosted by this ability and Multi-Lens should strike twice",
async () => { async () => {
game.override.moveset([Moves.SUPER_FANG]); game.override.moveset([Moves.SUPER_FANG]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
const enemyStartingHp = enemyPokemon.hp;
game.move.select(Moves.SUPER_FANG); game.move.select(Moves.SUPER_FANG);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(2); 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 }, TIMEOUT
); );
test( it(
"Seismic Toss boosted by this ability and Multi-Lens should strike twice", "Seismic Toss boosted by this ability and Multi-Lens should strike twice",
async () => { async () => {
game.override.moveset([Moves.SEISMIC_TOSS]); game.override.moveset([Moves.SEISMIC_TOSS]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
const enemyStartingHp = enemyPokemon.hp; const enemyStartingHp = enemyPokemon.hp;
game.move.select(Moves.SEISMIC_TOSS); game.move.select(Moves.SEISMIC_TOSS);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
await game.phaseInterceptor.to(MoveEndPhase, false); await game.phaseInterceptor.to("MoveEndPhase", false);
expect(enemyPokemon.hp).toBe(enemyStartingHp - 200); expect(enemyPokemon.hp).toBe(enemyStartingHp - 200);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"Hyper Beam boosted by this ability should strike twice, then recharge", "Hyper Beam boosted by this ability should strike twice, then recharge",
async () => { async () => {
game.override.moveset([Moves.HYPER_BEAM]); game.override.moveset([Moves.HYPER_BEAM]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; 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); game.move.select(Moves.HYPER_BEAM);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined(); expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined();
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeDefined(); expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeDefined();
}, TIMEOUT }, TIMEOUT
); );
/** TODO: Fix TRAPPED tag lapsing incorrectly, then run this test */ it(
test(
"Anchor Shot boosted by this ability should only trap the target after the second hit", "Anchor Shot boosted by this ability should only trap the target after the second hit",
async () => { async () => {
game.override.moveset([Moves.ANCHOR_SHOT]); game.override.moveset([Moves.ANCHOR_SHOT]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
game.move.select(Moves.ANCHOR_SHOT); game.move.select(Moves.ANCHOR_SHOT);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); // Passes expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined();
await game.phaseInterceptor.to(MoveEndPhase); await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Passes 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 }, TIMEOUT
); );
test( it(
"Smack Down boosted by this ability should only ground the target after the second hit", "Smack Down boosted by this ability should only ground the target after the second hit",
async () => { async () => {
game.override.moveset([Moves.SMACK_DOWN]); game.override.moveset([Moves.SMACK_DOWN]);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
game.move.select(Moves.SMACK_DOWN); game.move.select(Moves.SMACK_DOWN);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined(); expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined(); expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
}, TIMEOUT }, TIMEOUT
); );
test( it(
"U-turn boosted by this ability should strike twice before forcing a switch", "U-turn boosted by this ability should strike twice before forcing a switch",
async () => { async () => {
game.override.moveset([Moves.U_TURN]); 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()!; 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); game.move.select(Moves.U_TURN);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to("MoveEffectPhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
// This will cause this test to time out if the switch was forced on the first hit. // 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 }, TIMEOUT
); );
test( it(
"Wake-Up Slap boosted by this ability should only wake up the target after the second hit", "Wake-Up Slap boosted by this ability should only wake up the target after the second hit",
async () => { async () => {
game.override.moveset([Moves.WAKE_UP_SLAP]).enemyStatusEffect(StatusEffect.SLEEP); 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()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
game.move.select(Moves.WAKE_UP_SLAP); game.move.select(Moves.WAKE_UP_SLAP);
await game.move.forceHit(); await game.move.forceHit();
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(leadPokemon.turnData.hitCount).toBe(2); expect(leadPokemon.turnData.hitCount).toBe(2);
expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP); expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.status?.effect).toBeUndefined(); expect(enemyPokemon.status?.effect).toBeUndefined();
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not cause user to hit into King's Shield more than once", "should not cause user to hit into King's Shield more than once",
async () => { async () => {
game.override.moveset([Moves.TACKLE]); 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()!; 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); game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not cause user to hit into Storm Drain more than once", "should not cause user to hit into Storm Drain more than once",
async () => { async () => {
game.override.moveset([Moves.WATER_GUN]); game.override.moveset([Moves.WATER_GUN]);
game.override.enemyAbility(Abilities.STORM_DRAIN); game.override.enemyAbility(Abilities.STORM_DRAIN);
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()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).not.toBe(undefined);
game.move.select(Moves.WATER_GUN); 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); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, TIMEOUT }, TIMEOUT
); );
test( it(
"ability should not apply to multi-target moves with Multi-Lens", "should not apply to multi-target moves with Multi-Lens",
async () => { async () => {
game.override.battleType("double"); game.override.battleType("double");
game.override.moveset([Moves.EARTHQUAKE, Moves.SPLASH]); game.override.moveset([Moves.EARTHQUAKE, Moves.SPLASH]);
game.override.passiveAbility(Abilities.LEVITATE);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
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(); 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); const enemyStartingHp = enemyPokemon.map(p => p.hp);
game.move.select(Moves.EARTHQUAKE); game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1); 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); 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])); enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2 * enemyFirstHitDamage[i]));
}, TIMEOUT }, TIMEOUT
); );
}); });

View 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);
});

View 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);
});

View File

@ -0,0 +1,101 @@
import { BattlerIndex } from "#app/battle";
import { Type } from "#app/data/type";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Dragon Cheer", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("double")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(SPLASH_ONLY)
.enemyLevel(20)
.moveset([Moves.DRAGON_CHEER, Moves.TACKLE, Moves.SPLASH]);
});
it("increases the user's allies' critical hit ratio by one stage", async () => {
await game.classicMode.startBattle([Species.DRAGONAIR, Species.MAGIKARP]);
const enemy = game.scene.getEnemyField()[0];
vi.spyOn(enemy, "getCritStage");
game.move.select(Moves.DRAGON_CHEER, 0);
game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
// After Tackle
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender
}, TIMEOUT);
it("increases the user's Dragon-type allies' critical hit ratio by two stages", async () => {
await game.classicMode.startBattle([Species.MAGIKARP, Species.DRAGONAIR]);
const enemy = game.scene.getEnemyField()[0];
vi.spyOn(enemy, "getCritStage");
game.move.select(Moves.DRAGON_CHEER, 0);
game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
// After Tackle
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getCritStage).toHaveReturnedWith(2); // getCritStage is called on defender
}, TIMEOUT);
it("applies the effect based on the allies' type upon use of the move, and do not change if the allies' type changes later in battle", async () => {
await game.classicMode.startBattle([Species.DRAGONAIR, Species.MAGIKARP]);
const magikarp = game.scene.getPlayerField()[1];
const enemy = game.scene.getEnemyField()[0];
vi.spyOn(enemy, "getCritStage");
game.move.select(Moves.DRAGON_CHEER, 0);
game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
// After Tackle
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender
await game.toNextTurn();
// Change Magikarp's type to Dragon
vi.spyOn(magikarp, "getTypes").mockReturnValue([Type.DRAGON]);
expect(magikarp.getTypes()).toEqual([Type.DRAGON]);
game.move.select(Moves.SPLASH, 0);
game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY);
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender
}, TIMEOUT);
});

View File

@ -1,13 +1,12 @@
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { DamagePhase } from "#app/phases/damage-phase"; import { Moves } from "#app/enums/moves";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Species } from "#app/enums/species";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Moves - Glaive Rush", () => { describe("Moves - Glaive Rush", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -25,131 +24,142 @@ describe("Moves - Glaive Rush", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleType("single"); game.override
game.override.disableCrits(); .battleType("single")
game.override.enemySpecies(Species.MAGIKARP); .disableCrits()
game.override.enemyAbility(Abilities.BALL_FETCH); .enemySpecies(Species.MAGIKARP)
game.override.enemyMoveset(Array(4).fill(Moves.GLAIVE_RUSH)); .enemyAbility(Abilities.BALL_FETCH)
game.override.starterSpecies(Species.KLINK); .enemyMoveset(Array(4).fill(Moves.GLAIVE_RUSH))
game.override.ability(Abilities.UNNERVE); .starterSpecies(Species.KLINK)
game.override.passiveAbility(Abilities.FUR_COAT); .ability(Abilities.BALL_FETCH)
game.override.moveset([Moves.SHADOW_SNEAK, Moves.AVALANCHE, Moves.SPLASH, Moves.GLAIVE_RUSH]); .moveset([Moves.SHADOW_SNEAK, Moves.AVALANCHE, Moves.SPLASH, Moves.GLAIVE_RUSH]);
}); });
it("takes double damage from attacks", async () => { it("takes double damage from attacks", async () => {
await game.startBattle(); await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1000; enemy.hp = 1000;
vi.spyOn(game.scene, "randBattleSeedInt").mockReturnValue(0);
game.move.select(Moves.SHADOW_SNEAK); game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
const damageDealt = 1000 - enemy.hp; const damageDealt = 1000 - enemy.hp;
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.SHADOW_SNEAK); game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to(DamagePhase); await game.phaseInterceptor.to("DamagePhase");
expect(enemy.hp).toBeLessThanOrEqual(1001 - (damageDealt * 3)); expect(enemy.hp).toBeLessThanOrEqual(1001 - (damageDealt * 3));
}, 5000); // TODO: revert back to 20s }, TIMEOUT);
it("always gets hit by attacks", async () => { it("always gets hit by attacks", async () => {
await game.startBattle(); await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1000; enemy.hp = 1000;
allMoves[Moves.AVALANCHE].accuracy = 0; allMoves[Moves.AVALANCHE].accuracy = 0;
game.move.select(Moves.AVALANCHE); game.move.select(Moves.AVALANCHE);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.hp).toBeLessThan(1000); expect(enemy.hp).toBeLessThan(1000);
}, 20000); }, TIMEOUT);
it("interacts properly with multi-lens", async () => { it("interacts properly with multi-lens", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }]); game.override
game.override.enemyMoveset(Array(4).fill(Moves.AVALANCHE)); .startingHeldItems([{ name: "MULTI_LENS", count: 2 }])
await game.startBattle(); .enemyMoveset(Array(4).fill(Moves.AVALANCHE));
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1000; enemy.hp = 1000;
player.hp = 1000; player.hp = 1000;
allMoves[Moves.AVALANCHE].accuracy = 0; allMoves[Moves.AVALANCHE].accuracy = 0;
game.move.select(Moves.GLAIVE_RUSH); game.move.select(Moves.GLAIVE_RUSH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBeLessThan(1000); expect(player.hp).toBeLessThan(1000);
player.hp = 1000; player.hp = 1000;
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(1000); expect(player.hp).toBe(1000);
}, 20000); }, TIMEOUT);
it("secondary effects only last until next move", async () => { it("secondary effects only last until next move", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK)); game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK));
await game.startBattle(); await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1000; enemy.hp = 1000;
player.hp = 1000; player.hp = 1000;
allMoves[Moves.SHADOW_SNEAK].accuracy = 0; allMoves[Moves.SHADOW_SNEAK].accuracy = 0;
game.move.select(Moves.GLAIVE_RUSH); game.move.select(Moves.GLAIVE_RUSH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(1000); expect(player.hp).toBe(1000);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
const damagedHp = player.hp; const damagedHp = player.hp;
expect(player.hp).toBeLessThan(1000); expect(player.hp).toBeLessThan(1000);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(damagedHp); expect(player.hp).toBe(damagedHp);
}, 20000); }, TIMEOUT);
it("secondary effects are removed upon switching", async () => { it("secondary effects are removed upon switching", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK)); game.override
game.override.starterSpecies(0); .enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK))
await game.startBattle([Species.KLINK, Species.FEEBAS]); .starterSpecies(0);
await game.classicMode.startBattle([Species.KLINK, Species.FEEBAS]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1000; enemy.hp = 1000;
allMoves[Moves.SHADOW_SNEAK].accuracy = 0; allMoves[Moves.SHADOW_SNEAK].accuracy = 0;
game.move.select(Moves.GLAIVE_RUSH); game.move.select(Moves.GLAIVE_RUSH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(player.getMaxHp()); expect(player.hp).toBe(player.getMaxHp());
game.doSwitchPokemon(1); game.doSwitchPokemon(1);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
game.doSwitchPokemon(1); game.doSwitchPokemon(1);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(player.hp).toBe(player.getMaxHp()); expect(player.hp).toBe(player.getMaxHp());
}, 20000); }, TIMEOUT);
it("secondary effects don't activate if move fails", async () => { it("secondary effects don't activate if move fails", async () => {
game.override.moveset([Moves.SHADOW_SNEAK, Moves.PROTECT, Moves.SPLASH, Moves.GLAIVE_RUSH]); game.override.moveset([Moves.SHADOW_SNEAK, Moves.PROTECT, Moves.SPLASH, Moves.GLAIVE_RUSH]);
await game.startBattle(); await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1000; enemy.hp = 1000;
player.hp = 1000; player.hp = 1000;
game.move.select(Moves.PROTECT); game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
game.move.select(Moves.SHADOW_SNEAK); game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
game.override.enemyMoveset(Array(4).fill(Moves.SPLASH)); game.override.enemyMoveset(Array(4).fill(Moves.SPLASH));
const damagedHP1 = 1000 - enemy.hp; const damagedHP1 = 1000 - enemy.hp;
enemy.hp = 1000; enemy.hp = 1000;
game.move.select(Moves.SHADOW_SNEAK); game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
const damagedHP2 = 1000 - enemy.hp; const damagedHP2 = 1000 - enemy.hp;
expect(damagedHP2).toBeGreaterThanOrEqual((damagedHP1 * 2) - 1); expect(damagedHP2).toBeGreaterThanOrEqual((damagedHP1 * 2) - 1);
}, 20000); }, TIMEOUT);
}); });

View File

@ -7,7 +7,8 @@ import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
import { BerryPhase } from "#app/phases/berry-phase"; import { BattlerIndex } from "#app/battle";
import { MoveResult } from "#app/field/pokemon";
const TIMEOUT = 20 * 1000; const TIMEOUT = 20 * 1000;
@ -43,13 +44,13 @@ describe("Moves - Protect", () => {
test( test(
"should protect the user from attacks", "should protect the user from attacks",
async () => { async () => {
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.PROTECT); game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
}, TIMEOUT }, TIMEOUT
@ -61,13 +62,13 @@ describe("Moves - Protect", () => {
game.override.enemyMoveset(Array(4).fill(Moves.CEASELESS_EDGE)); game.override.enemyMoveset(Array(4).fill(Moves.CEASELESS_EDGE));
vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100); vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100);
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.PROTECT); game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined(); expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeUndefined();
@ -79,13 +80,13 @@ describe("Moves - Protect", () => {
async () => { async () => {
game.override.enemyMoveset(Array(4).fill(Moves.CHARM)); game.override.enemyMoveset(Array(4).fill(Moves.CHARM));
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.PROTECT); game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0); expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
}, TIMEOUT }, TIMEOUT
@ -96,18 +97,38 @@ describe("Moves - Protect", () => {
async () => { async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACHYON_CUTTER)); game.override.enemyMoveset(Array(4).fill(Moves.TACHYON_CUTTER));
await game.startBattle([Species.CHARIZARD]); await game.classicMode.startBattle([Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.PROTECT); game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp());
expect(enemyPokemon.turnData.hitCount).toBe(1); expect(enemyPokemon.turnData.hitCount).toBe(1);
}, TIMEOUT }, TIMEOUT
); );
test(
"should fail if the user is the last to move in the turn",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.PROTECT));
await game.classicMode.startBattle([Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.PROTECT);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(leadPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
}, TIMEOUT
);
}); });

View File

@ -5,8 +5,8 @@ import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { BerryPhase } from "#app/phases/berry-phase"; import { BattlerIndex } from "#app/battle";
import { CommandPhase } from "#app/phases/command-phase"; import { MoveResult } from "#app/field/pokemon";
const TIMEOUT = 20 * 1000; const TIMEOUT = 20 * 1000;
@ -42,19 +42,16 @@ describe("Moves - Quick Guard", () => {
test( test(
"should protect the user and allies from priority moves", "should protect the user and allies from priority moves",
async () => { async () => {
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerField(); const playerPokemon = game.scene.getPlayerField();
game.move.select(Moves.QUICK_GUARD); game.move.select(Moves.QUICK_GUARD);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
}, TIMEOUT }, TIMEOUT
); );
@ -64,19 +61,16 @@ describe("Moves - Quick Guard", () => {
game.override.enemyAbility(Abilities.PRANKSTER); game.override.enemyAbility(Abilities.PRANKSTER);
game.override.enemyMoveset(Array(4).fill(Moves.GROWL)); game.override.enemyMoveset(Array(4).fill(Moves.GROWL));
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerField(); const playerPokemon = game.scene.getPlayerField();
game.move.select(Moves.QUICK_GUARD); game.move.select(Moves.QUICK_GUARD);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
leadPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); playerPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0));
}, TIMEOUT }, TIMEOUT
); );
@ -85,21 +79,40 @@ describe("Moves - Quick Guard", () => {
async () => { async () => {
game.override.enemyMoveset(Array(4).fill(Moves.WATER_SHURIKEN)); game.override.enemyMoveset(Array(4).fill(Moves.WATER_SHURIKEN));
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); await game.classicMode.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerField(); const playerPokemon = game.scene.getPlayerField();
const enemyPokemon = game.scene.getEnemyField(); const enemyPokemon = game.scene.getEnemyField();
game.move.select(Moves.QUICK_GUARD); game.move.select(Moves.QUICK_GUARD);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.FOLLOW_ME, 1); game.move.select(Moves.FOLLOW_ME, 1);
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
leadPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); playerPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp()));
enemyPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1)); enemyPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
} }
); );
test(
"should fail if the user is the last to move in the turn",
async () => {
game.override.battleType("single");
game.override.enemyMoveset(Array(4).fill(Moves.QUICK_GUARD));
await game.classicMode.startBattle([Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.QUICK_GUARD);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
}, TIMEOUT
);
}); });

View File

@ -25,7 +25,6 @@ describe("Moves - Rage Powder", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleType("double"); game.override.battleType("double");
game.override.starterSpecies(Species.AMOONGUSS);
game.override.enemySpecies(Species.SNORLAX); game.override.enemySpecies(Species.SNORLAX);
game.override.startingLevel(100); game.override.startingLevel(100);
game.override.enemyLevel(100); game.override.enemyLevel(100);
@ -68,6 +67,10 @@ describe("Moves - Rage Powder", () => {
game.move.select(Moves.QUICK_ATTACK, 0, BattlerIndex.ENEMY); game.move.select(Moves.QUICK_ATTACK, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2); game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
await game.forceEnemyMove(Moves.RAGE_POWDER);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
// If redirection was bypassed, both enemies should be damaged // If redirection was bypassed, both enemies should be damaged

View File

@ -76,7 +76,7 @@ export default class GameManager {
constructor(phaserGame: Phaser.Game, bypassLogin: boolean = true) { constructor(phaserGame: Phaser.Game, bypassLogin: boolean = true) {
localStorage.clear(); localStorage.clear();
ErrorInterceptor.getInstance().clear(); ErrorInterceptor.getInstance().clear();
BattleScene.prototype.randBattleSeedInt = (arg) => arg-1; BattleScene.prototype.randBattleSeedInt = (range, min: number = 0) => min + range - 1; // This simulates a max roll
this.gameWrapper = new GameWrapper(phaserGame, bypassLogin); this.gameWrapper = new GameWrapper(phaserGame, bypassLogin);
this.scene = new BattleScene(); this.scene = new BattleScene();
this.phaseInterceptor = new PhaseInterceptor(this.scene); this.phaseInterceptor = new PhaseInterceptor(this.scene);

View File

@ -208,4 +208,5 @@ export default class MockContainer implements MockGameObject {
return this.list; return this.list;
} }
disableInteractive = vi.fn();
} }

View File

@ -1,5 +1,6 @@
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
import ErrorInterceptor from "#app/test/utils/errorInterceptor"; import ErrorInterceptor from "#app/test/utils/errorInterceptor";
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { BerryPhase } from "#app/phases/berry-phase"; import { BerryPhase } from "#app/phases/berry-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
@ -100,6 +101,7 @@ export default class PhaseInterceptor {
[EvolutionPhase, this.startPhase], [EvolutionPhase, this.startPhase],
[EndEvolutionPhase, this.startPhase], [EndEvolutionPhase, this.startPhase],
[LevelCapPhase, this.startPhase], [LevelCapPhase, this.startPhase],
[AttemptRunPhase, this.startPhase],
]; ];
private endBySetMode = [ private endBySetMode = [

View File

@ -3,14 +3,31 @@ import { ModalConfig } from "./modal-ui-handler";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { Mode } from "./ui"; import { Mode } from "./ui";
import i18next from "i18next"; import i18next from "i18next";
import BattleScene from "#app/battle-scene.js"; import BattleScene from "#app/battle-scene";
import { addTextObject, TextStyle } from "./text"; import { addTextObject, TextStyle } from "./text";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
interface BuildInteractableImageOpts {
scale?: number;
x?: number;
y?: number;
origin?: { x: number; y: number };
}
export default class LoginFormUiHandler extends FormModalUiHandler { export default class LoginFormUiHandler extends FormModalUiHandler {
private readonly ERR_USERNAME: string = "invalid username";
private readonly ERR_PASSWORD: string = "invalid password";
private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist";
private readonly ERR_PASSWORD_MATCH: string = "password doesn't match";
private readonly ERR_NO_SAVES: string = "No save files found";
private readonly ERR_TOO_MANY_SAVES: string = "Too many save files found";
private googleImage: Phaser.GameObjects.Image; private googleImage: Phaser.GameObjects.Image;
private discordImage: Phaser.GameObjects.Image; private discordImage: Phaser.GameObjects.Image;
private usernameInfoImage: Phaser.GameObjects.Image;
private externalPartyContainer: Phaser.GameObjects.Container; private externalPartyContainer: Phaser.GameObjects.Container;
private infoContainer: Phaser.GameObjects.Container;
private externalPartyBg: Phaser.GameObjects.NineSlice; private externalPartyBg: Phaser.GameObjects.NineSlice;
private externalPartyTitle: Phaser.GameObjects.Text; private externalPartyTitle: Phaser.GameObjects.Text;
constructor(scene: BattleScene, mode: Mode | null = null) { constructor(scene: BattleScene, mode: Mode | null = null) {
@ -18,8 +35,23 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
} }
setup(): void { setup(): void {
super.setup(); super.setup();
this.buildExternalPartyContainer();
this.infoContainer = this.scene.add.container(0, 0);
this.usernameInfoImage = this.buildInteractableImage("settings_icon", "username-info-icon", {
x: 20,
scale: 0.5
});
this.infoContainer.add(this.usernameInfoImage);
this.getUi().add(this.infoContainer);
this.infoContainer.setVisible(false);
this.infoContainer.disableInteractive();
}
private buildExternalPartyContainer() {
this.externalPartyContainer = this.scene.add.container(0, 0); this.externalPartyContainer = this.scene.add.container(0, 0);
this.externalPartyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 12, this.scene.game.canvas.height / 12), Phaser.Geom.Rectangle.Contains); this.externalPartyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 12, this.scene.game.canvas.height / 12), Phaser.Geom.Rectangle.Contains);
this.externalPartyTitle = addTextObject(this.scene, 0, 4, "", TextStyle.SETTINGS_LABEL); this.externalPartyTitle = addTextObject(this.scene, 0, 4, "", TextStyle.SETTINGS_LABEL);
@ -28,31 +60,8 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.externalPartyContainer.add(this.externalPartyBg); this.externalPartyContainer.add(this.externalPartyBg);
this.externalPartyContainer.add(this.externalPartyTitle); this.externalPartyContainer.add(this.externalPartyTitle);
const googleImage = this.scene.add.image(0, 0, "google"); this.googleImage = this.buildInteractableImage("google", "google-icon");
googleImage.setOrigin(0, 0); this.discordImage = this.buildInteractableImage("discord", "discord-icon");
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");
discordImage.setOrigin(0, 0);
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); this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage); this.externalPartyContainer.add(this.discordImage);
@ -62,51 +71,54 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.externalPartyContainer.setVisible(false); this.externalPartyContainer.setVisible(false);
} }
getModalTitle(config?: ModalConfig): string { override getModalTitle(_config?: ModalConfig): string {
return i18next.t("menu:login"); return i18next.t("menu:login");
} }
getFields(config?: ModalConfig): string[] { override getFields(_config?: ModalConfig): string[] {
return [ i18next.t("menu:username"), i18next.t("menu:password") ]; return [ i18next.t("menu:username"), i18next.t("menu:password") ];
} }
getWidth(config?: ModalConfig): number { override getWidth(_config?: ModalConfig): number {
return 160; return 160;
} }
getMargin(config?: ModalConfig): [number, number, number, number] { override getMargin(_config?: ModalConfig): [number, number, number, number] {
return [ 0, 0, 48, 0 ]; return [ 0, 0, 48, 0 ];
} }
getButtonLabels(config?: ModalConfig): string[] { override getButtonLabels(_config?: ModalConfig): string[] {
return [ i18next.t("menu:login"), i18next.t("menu:register")]; return [ i18next.t("menu:login"), i18next.t("menu:register")];
} }
getReadableErrorMessage(error: string): string { override getReadableErrorMessage(error: string): string {
const colonIndex = error?.indexOf(":"); const colonIndex = error?.indexOf(":");
if (colonIndex > 0) { if (colonIndex > 0) {
error = error.slice(0, colonIndex); error = error.slice(0, colonIndex);
} }
switch (error) { switch (error) {
case "invalid username": case this.ERR_USERNAME:
return i18next.t("menu:invalidLoginUsername"); return i18next.t("menu:invalidLoginUsername");
case "invalid password": case this.ERR_PASSWORD:
return i18next.t("menu:invalidLoginPassword"); return i18next.t("menu:invalidLoginPassword");
case "account doesn't exist": case this.ERR_ACCOUNT_EXIST:
return i18next.t("menu:accountNonExistent"); return i18next.t("menu:accountNonExistent");
case "password doesn't match": case this.ERR_PASSWORD_MATCH:
return i18next.t("menu:unmatchingPassword"); return i18next.t("menu:unmatchingPassword");
case this.ERR_NO_SAVES:
return i18next.t("menu:noSaves");
case this.ERR_TOO_MANY_SAVES:
return i18next.t("menu:tooManySaves");
} }
return super.getReadableErrorMessage(error); return super.getReadableErrorMessage(error);
} }
show(args: any[]): boolean { override show(args: any[]): boolean {
if (super.show(args)) { if (super.show(args)) {
this.processExternalProvider();
const config = args[0] as ModalConfig; const config = args[0] as ModalConfig;
this.processExternalProvider(config);
const originalLoginAction = this.submitAction; const originalLoginAction = this.submitAction;
this.submitAction = (_) => { this.submitAction = (_) => {
// Prevent overlapping overrides on action modification // Prevent overlapping overrides on action modification
@ -143,25 +155,81 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
return false; return false;
} }
clear() { override clear() {
super.clear(); super.clear();
this.externalPartyContainer.setVisible(false); this.externalPartyContainer.setVisible(false);
this.infoContainer.setVisible(false);
this.setMouseCursorStyle("default"); //reset cursor
this.discordImage.off("pointerdown"); [this.discordImage, this.googleImage, this.usernameInfoImage].forEach((img) => img.off("pointerdown"));
this.googleImage.off("pointerdown");
} }
processExternalProvider() : void { private processExternalProvider(config: ModalConfig) : void {
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? ""); this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length); this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length);
this.externalPartyTitle.setVisible(true); this.externalPartyTitle.setVisible(true);
this.externalPartyContainer.setPositionRelative(this.modalContainer, 175, 0); this.externalPartyContainer.setPositionRelative(this.modalContainer, 175, 0);
this.externalPartyContainer.setVisible(true); 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.getUi().moveTo(this.externalPartyContainer, this.getUi().length - 1);
this.googleImage.setPosition(this.externalPartyBg.width/3.1, this.externalPartyBg.height-60); 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.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();
this.infoContainer.disableInteractive();
return true;
}
});
}
this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
options: options,
delay: 1000
});
this.infoContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width, this.scene.game.canvas.height), Phaser.Geom.Rectangle.Contains);
} else {
if (dataKeys.length > 2) {
return onFail(this.ERR_TOO_MANY_SAVES);
} else {
return onFail(this.ERR_NO_SAVES);
}
}
});
this.externalPartyContainer.setAlpha(0); this.externalPartyContainer.setAlpha(0);
this.scene.tweens.add({ this.scene.tweens.add({
targets: this.externalPartyContainer, targets: this.externalPartyContainer,
@ -170,5 +238,31 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
y: "-=24", y: "-=24",
alpha: 1 alpha: 1
}); });
this.infoContainer.setAlpha(0);
this.scene.tweens.add({
targets: this.infoContainer,
duration: Utils.fixedInt(1000),
ease: "Sine.easeInOut",
y: "-=24",
alpha: 1
});
}
private buildInteractableImage(texture: string, name: string, opts: BuildInteractableImageOpts = {}) {
const {
scale = 0.07,
x = 0,
y = 0,
origin = { x: 0, y: 0 }
} = opts;
const img = this.scene.add.image(x, y, texture);
img.setName(name);
img.setOrigin(origin.x, origin.y);
img.setScale(scale);
img.setInteractive();
this.addInteractionHoverEffect(img);
return img;
} }
} }

View File

@ -57,29 +57,35 @@ export abstract class ModalUiHandler extends UiHandler {
const buttonLabels = this.getButtonLabels(); const buttonLabels = this.getButtonLabels();
const buttonTopMargin = this.getButtonTopMargin();
for (const label of buttonLabels) { for (const label of buttonLabels) {
const buttonLabel = addTextObject(this.scene, 0, 8, label, TextStyle.TOOLTIP_CONTENT); this.addButton(label);
buttonLabel.setOrigin(0.5, 0.5);
const buttonBg = addWindow(this.scene, 0, 0, buttonLabel.getBounds().width + 8, 16, false, false, 0, 0, WindowVariant.THIN);
buttonBg.setOrigin(0.5, 0);
buttonBg.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonBg.width, buttonBg.height), Phaser.Geom.Rectangle.Contains);
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
this.buttonBgs.push(buttonBg);
this.buttonContainers.push(buttonContainer);
buttonContainer.add(buttonBg);
buttonContainer.add(buttonLabel);
this.modalContainer.add(buttonContainer);
} }
this.modalContainer.setVisible(false); this.modalContainer.setVisible(false);
} }
private addButton(label: string) {
const buttonTopMargin = this.getButtonTopMargin();
const buttonLabel = addTextObject(this.scene, 0, 8, label, TextStyle.TOOLTIP_CONTENT);
buttonLabel.setOrigin(0.5, 0.5);
const buttonBg = addWindow(this.scene, 0, 0, buttonLabel.getBounds().width + 8, 16, false, false, 0, 0, WindowVariant.THIN);
buttonBg.setOrigin(0.5, 0);
buttonBg.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonBg.width, buttonBg.height), Phaser.Geom.Rectangle.Contains);
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
this.buttonBgs.push(buttonBg);
this.buttonContainers.push(buttonContainer);
buttonContainer.add(buttonBg);
buttonContainer.add(buttonLabel);
this.addInteractionHoverEffect(buttonBg);
this.modalContainer.add(buttonContainer);
}
show(args: any[]): boolean { show(args: any[]): boolean {
if (args.length >= 1 && "buttonActions" in args[0]) { if (args.length >= 1 && "buttonActions" in args[0]) {
super.show(args); super.show(args);
@ -135,4 +141,20 @@ export abstract class ModalUiHandler extends UiHandler {
this.buttonBgs.map(bg => bg.off("pointerdown")); this.buttonBgs.map(bg => bg.off("pointerdown"));
} }
/**
* Adds a hover effect to a game object which changes the cursor to a `pointer` and tints it slighly
* @param gameObject the game object to add hover events/effects to
*/
protected addInteractionHoverEffect(gameObject: Phaser.GameObjects.Image | Phaser.GameObjects.NineSlice | Phaser.GameObjects.Sprite) {
gameObject.on("pointerover", () => {
this.setMouseCursorStyle("pointer");
gameObject.setTint(0xbbbbbb);
});
gameObject.on("pointerout", () => {
this.setMouseCursorStyle("default");
gameObject.clearTint();
});
}
} }

View File

@ -5,6 +5,20 @@ import { Mode } from "./ui";
import { TextStyle, addTextObject } from "./text"; import { TextStyle, addTextObject } from "./text";
import i18next from "i18next"; 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 { export default class RegistrationFormUiHandler extends FormModalUiHandler {
getModalTitle(config?: ModalConfig): string { getModalTitle(config?: ModalConfig): string {
return i18next.t("menu:register"); return i18next.t("menu:register");
@ -50,7 +64,17 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
setup(): void { setup(): void {
super.setup(); 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); this.modalContainer.add(label);
} }
@ -68,6 +92,10 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
const onFail = error => { const onFail = error => {
this.scene.ui.setMode(Mode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() })); this.scene.ui.setMode(Mode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
this.scene.ui.playError(); this.scene.ui.playError();
const errorMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.errorMessageFontSize;
if (errorMessageFontSize) {
this.errorMessage.setFontSize(errorMessageFontSize);
}
}; };
if (!this.inputs[0].text) { if (!this.inputs[0].text) {
return onFail(i18next.t("menu:emptyUsername")); return onFail(i18next.t("menu:emptyUsername"));

View File

@ -52,6 +52,15 @@ export default abstract class UiHandler {
return changed; return changed;
} }
/**
* Changes the style of the mouse cursor.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/cursor}
* @param cursorStyle cursor style to apply
*/
protected setMouseCursorStyle(cursorStyle: "pointer" | "default") {
this.scene.input.manager.canvas.style.cursor = cursorStyle;
}
clear() { clear() {
this.active = false; this.active = false;
} }

View File

@ -1,5 +1,5 @@
import i18next from "i18next";
import { MoneyFormat } from "#enums/money-format"; import { MoneyFormat } from "#enums/money-format";
import i18next from "i18next";
export const MissingTextureKey = "__MISSING"; export const MissingTextureKey = "__MISSING";
@ -82,6 +82,12 @@ export function randInt(range: integer, min: integer = 0): integer {
return Math.floor(Math.random() * range) + min; return Math.floor(Math.random() * range) + min;
} }
/**
* Generates a random number using the global seed, or the current battle's seed if called via `Battle.randSeedInt`
* @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
* @param min The minimum integer to pick, default `0`
* @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
*/
export function randSeedInt(range: integer, min: integer = 0): integer { export function randSeedInt(range: integer, min: integer = 0): integer {
if (range <= 1) { if (range <= 1) {
return min; return min;