Merge branch 'beta' into mystery-encounters-translations

This commit is contained in:
Lugiad 2024-09-12 04:47:04 +02:00 committed by GitHub
commit 6726b1ce69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 592 additions and 360 deletions

View File

@ -1,7 +1,7 @@
name: Bug Report
description: Create a report to help us improve
title: "[Bug] "
labels: ["Bug"]
labels: ["Bug", "Triage"]
body:
- type: markdown
attributes:
@ -19,21 +19,12 @@ body:
value: |
---
- type: textarea
id: session-file
id: repro
attributes:
label: Session export file
description: Open Menu → ManageData → Export Session → Select slot. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
label: Reproduction
description: Describe the steps to reproduce this bug. If applicable attach user/session data at the bottom
validations:
required: false
- type: textarea
id: data-file
attributes:
label: User data export file
description: Open Menu → ManageData → Export Data. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
validations:
required: false
required: true
- type: markdown
attributes:
value: |
@ -60,48 +51,20 @@ body:
attributes:
value: |
---
- type: dropdown
id: os
- type: textarea
id: session-file
attributes:
label: What OS did you observe the bug on?
multiple: true
options:
- PC/Windows
- Mac/OSX
- Linux
- iOS
- Android
- Other
validations:
required: true
- type: input
id: os-other
attributes:
label: If other please specify
label: Session export file
description: Open Menu → ManageData → Export Session → Select slot. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
validations:
required: false
- type: markdown
- type: textarea
id: data-file
attributes:
value: |
---
- type: dropdown
id: browser
attributes:
label: Which browser do you use?
multiple: true
options:
- Chrome
- Firefox
- Safari
- Edge
- Opera
- Other
validations:
required: true
- type: input
id: browser-other
attributes:
label: If other please specify
label: User data export file
description: Open Menu → ManageData → Export Data. The file should now be in your `/Downloads` directory. Change the file extension type from `.prsv` to `.txt` (How to [Windows](https://www.guidingtech.com/how-to-change-file-type-on-windows/) | [Mac](https://support.apple.com/guide/mac-help/show-or-hide-filename-extensions-on-mac-mchlp2304/mac) | [iOS](https://www.guidingtech.com/change-file-type-extension-on-iphone/)).
placeholder: Focus me and then drop your file here (or use the upload button at the bottom)
validations:
required: false
- type: markdown

View File

@ -1,7 +1,7 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature] "
labels: ["Enhancement"]
labels: ["Enhancement", "Triage"]
body:
- type: markdown
attributes:

View File

@ -1,5 +1,4 @@
import BattleScene from "./battle-scene";
import { EnemyPokemon, PlayerPokemon, QueuedMove } from "./field/pokemon";
import { Command } from "./ui/command-ui-handler";
import * as Utils from "./utils";
import Trainer, { TrainerVariant } from "./field/trainer";
@ -7,6 +6,7 @@ import { GameMode } from "./game-mode";
import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball";
import { trainerConfigs } from "#app/data/trainer-config";
import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
import { Moves } from "#enums/moves";
@ -38,6 +38,11 @@ export interface TurnCommand {
args?: any[];
}
export interface FaintLogEntry {
pokemon: Pokemon,
turn: number
}
interface TurnCommands {
[key: number]: TurnCommand | null
}
@ -69,6 +74,8 @@ export default class Battle {
public playerFaints: number = 0;
/** The number of times a Pokemon on the enemy's side has fainted this battle */
public enemyFaints: number = 0;
public playerFaintsHistory: FaintLogEntry[] = [];
public enemyFaintsHistory: FaintLogEntry[] = [];
private rngCounter: number = 0;

View File

@ -1298,6 +1298,13 @@ export class ProtectedTag extends BattlerTag {
}
}
/** Base class for `BattlerTag`s that block damaging moves but not status moves */
export class DamageProtectedTag extends ProtectedTag {}
/**
* `BattlerTag` class for moves that block damaging moves damage the enemy if the enemy's move makes contact
* Used by {@linkcode Moves.SPIKY_SHIELD}
*/
export class ContactDamageProtectedTag extends ProtectedTag {
private damageRatio: number;
@ -1333,7 +1340,11 @@ export class ContactDamageProtectedTag extends ProtectedTag {
}
}
export class ContactStatStageChangeProtectedTag extends ProtectedTag {
/**
* `BattlerTag` class for moves that block damaging moves and lower enemy stats if the enemy's move makes contact
* Used by {@linkcode Moves.KINGS_SHIELD}, {@linkcode Moves.OBSTRUCT}, {@linkcode Moves.SILK_TRAP}
*/
export class ContactStatStageChangeProtectedTag extends DamageProtectedTag {
private stat: BattleStat;
private levels: number;
@ -1389,7 +1400,11 @@ export class ContactPoisonProtectedTag extends ProtectedTag {
}
}
export class ContactBurnProtectedTag extends ProtectedTag {
/**
* `BattlerTag` class for moves that block damaging moves and burn the enemy if the enemy's move makes contact
* Used by {@linkcode Moves.BURNING_BULWARK}
*/
export class ContactBurnProtectedTag extends DamageProtectedTag {
constructor(sourceMove: Moves) {
super(sourceMove, BattlerTagType.BURNING_BULWARK);
}

View File

@ -81,6 +81,16 @@ export enum MoveFlags {
MAKES_CONTACT = 1 << 0,
IGNORE_PROTECT = 1 << 1,
IGNORE_VIRTUAL = 1 << 2,
/**
* Sound-based moves have the following effects:
* - Pokemon with the {@linkcode Abilities.SOUNDPROOF Soundproof Ability} are unaffected by other Pokemon's sound-based moves.
* - Pokemon affected by {@linkcode Moves.THROAT_CHOP Throat Chop} cannot use sound-based moves for two turns.
* - Sound-based moves used by a Pokemon with {@linkcode Abilities.LIQUID_VOICE Liquid Voice} become Water-type moves.
* - Sound-based moves used by a Pokemon with {@linkcode Abilities.PUNK_ROCK Punk Rock} are boosted by 30%. Pokemon with Punk Rock also take half damage from sound-based moves.
* - All sound-based moves (except Howl) can hit Pokemon behind an active {@linkcode Moves.SUBSTITUTE Substitute}.
*
* cf https://bulbapedia.bulbagarden.net/wiki/Sound-based_move
*/
SOUND_BASED = 1 << 3,
HIDE_USER = 1 << 4,
HIDE_TARGET = 1 << 5,
@ -93,19 +103,20 @@ export enum MoveFlags {
* @see {@linkcode Move.recklessMove()}
*/
RECKLESS_MOVE = 1 << 10,
/** Indicates a move should be affected by {@linkcode Abilities.BULLETPROOF} */
BALLBOMB_MOVE = 1 << 11,
/** Grass types and pokemon with {@linkcode Abilities.OVERCOAT} are immune to powder moves */
POWDER_MOVE = 1 << 12,
/** Indicates a move should trigger {@linkcode Abilities.DANCER} */
DANCE_MOVE = 1 << 13,
/** Indicates a move should trigger {@linkcode Abilities.WIND_RIDER} */
WIND_MOVE = 1 << 14,
/** Indicates a move should trigger {@linkcode Abilities.TRIAGE} */
TRIAGE_MOVE = 1 << 15,
IGNORE_ABILITIES = 1 << 16,
/**
* Enables all hits of a multi-hit move to be accuracy checked individually
*/
/** Enables all hits of a multi-hit move to be accuracy checked individually */
CHECK_ALL_HITS = 1 << 17,
/**
* Indicates a move is able to be redirected to allies in a double battle if the attacker faints
*/
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
REDIRECT_COUNTER = 1 << 18,
}
@ -118,22 +129,22 @@ export default class Move implements Localizable {
private _type: Type;
private _category: MoveCategory;
public moveTarget: MoveTarget;
public power: integer;
public accuracy: integer;
public pp: integer;
public power: number;
public accuracy: number;
public pp: number;
public effect: string;
public chance: integer;
public priority: integer;
public generation: integer;
public attrs: MoveAttr[];
private conditions: MoveCondition[];
private flags: integer;
private nameAppend: string;
/** The chance of a move's secondary effects activating */
public chance: number;
public priority: number;
public generation: number;
public attrs: MoveAttr[] = [];
private conditions: MoveCondition[] = [];
/** The move's {@linkcode MoveFlags} */
private flags: number = 0;
private nameAppend: string = "";
constructor(id: Moves, type: Type, category: MoveCategory, defaultMoveTarget: MoveTarget, power: integer, accuracy: integer, pp: integer, chance: integer, priority: integer, generation: integer) {
constructor(id: Moves, type: Type, category: MoveCategory, defaultMoveTarget: MoveTarget, power: number, accuracy: number, pp: number, chance: number, priority: number, generation: number) {
this.id = id;
this.nameAppend = "";
this._type = type;
this._category = category;
this.moveTarget = defaultMoveTarget;
@ -144,10 +155,6 @@ export default class Move implements Localizable {
this.priority = priority;
this.generation = generation;
this.attrs = [];
this.conditions = [];
this.flags = 0;
if (defaultMoveTarget === MoveTarget.USER) {
this.setFlag(MoveFlags.IGNORE_PROTECT, true);
}
@ -377,7 +384,7 @@ export default class Move implements Localizable {
* @param makesContact The value (boolean) to set the flag to
* @returns The {@linkcode Move} that called this function
*/
makesContact(makesContact: boolean = true): this { // TODO: is true the correct default?
makesContact(makesContact: boolean = true): this {
this.setFlag(MoveFlags.MAKES_CONTACT, makesContact);
return this;
}
@ -388,7 +395,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.CURSE}
* @returns The {@linkcode Move} that called this function
*/
ignoresProtect(ignoresProtect: boolean = true): this { // TODO: is `true` the correct default?
ignoresProtect(ignoresProtect: boolean = true): this {
this.setFlag(MoveFlags.IGNORE_PROTECT, ignoresProtect);
return this;
}
@ -399,7 +406,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.NATURE_POWER}
* @returns The {@linkcode Move} that called this function
*/
ignoresVirtual(ignoresVirtual: boolean = true): this { // TODO: is `true` the correct default?
ignoresVirtual(ignoresVirtual: boolean = true): this {
this.setFlag(MoveFlags.IGNORE_VIRTUAL, ignoresVirtual);
return this;
}
@ -410,7 +417,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.UPROAR}
* @returns The {@linkcode Move} that called this function
*/
soundBased(soundBased: boolean = true): this { // TODO: is `true` the correct default?
soundBased(soundBased: boolean = true): this {
this.setFlag(MoveFlags.SOUND_BASED, soundBased);
return this;
}
@ -421,7 +428,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.TELEPORT}
* @returns The {@linkcode Move} that called this function
*/
hidesUser(hidesUser: boolean = true): this { // TODO: is `true` the correct default?
hidesUser(hidesUser: boolean = true): this {
this.setFlag(MoveFlags.HIDE_USER, hidesUser);
return this;
}
@ -432,7 +439,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.WHIRLWIND}
* @returns The {@linkcode Move} that called this function
*/
hidesTarget(hidesTarget: boolean = true): this { // TODO: is `true` the correct default?
hidesTarget(hidesTarget: boolean = true): this {
this.setFlag(MoveFlags.HIDE_TARGET, hidesTarget);
return this;
}
@ -443,7 +450,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.BITE}
* @returns The {@linkcode Move} that called this function
*/
bitingMove(bitingMove: boolean = true): this { // TODO: is `true` the correct default?
bitingMove(bitingMove: boolean = true): this {
this.setFlag(MoveFlags.BITING_MOVE, bitingMove);
return this;
}
@ -454,7 +461,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.WATER_PULSE}
* @returns The {@linkcode Move} that called this function
*/
pulseMove(pulseMove: boolean = true): this { // TODO: is `true` the correct default?
pulseMove(pulseMove: boolean = true): this {
this.setFlag(MoveFlags.PULSE_MOVE, pulseMove);
return this;
}
@ -465,7 +472,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.DRAIN_PUNCH}
* @returns The {@linkcode Move} that called this function
*/
punchingMove(punchingMove: boolean = true): this { // TODO: is `true` the correct default?
punchingMove(punchingMove: boolean = true): this {
this.setFlag(MoveFlags.PUNCHING_MOVE, punchingMove);
return this;
}
@ -476,7 +483,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.X_SCISSOR}
* @returns The {@linkcode Move} that called this function
*/
slicingMove(slicingMove: boolean = true): this { // TODO: is `true` the correct default?
slicingMove(slicingMove: boolean = true): this {
this.setFlag(MoveFlags.SLICING_MOVE, slicingMove);
return this;
}
@ -487,7 +494,7 @@ export default class Move implements Localizable {
* @param recklessMove The value to set the flag to
* @returns The {@linkcode Move} that called this function
*/
recklessMove(recklessMove: boolean = true): this { // TODO: is `true` the correct default?
recklessMove(recklessMove: boolean = true): this {
this.setFlag(MoveFlags.RECKLESS_MOVE, recklessMove);
return this;
}
@ -498,7 +505,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.ELECTRO_BALL}
* @returns The {@linkcode Move} that called this function
*/
ballBombMove(ballBombMove: boolean = true): this { // TODO: is `true` the correct default?
ballBombMove(ballBombMove: boolean = true): this {
this.setFlag(MoveFlags.BALLBOMB_MOVE, ballBombMove);
return this;
}
@ -509,7 +516,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.STUN_SPORE}
* @returns The {@linkcode Move} that called this function
*/
powderMove(powderMove: boolean = true): this { // TODO: is `true` the correct default?
powderMove(powderMove: boolean = true): this {
this.setFlag(MoveFlags.POWDER_MOVE, powderMove);
return this;
}
@ -520,7 +527,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.PETAL_DANCE}
* @returns The {@linkcode Move} that called this function
*/
danceMove(danceMove: boolean = true): this { // TODO: is `true` the correct default?
danceMove(danceMove: boolean = true): this {
this.setFlag(MoveFlags.DANCE_MOVE, danceMove);
return this;
}
@ -531,7 +538,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.HURRICANE}
* @returns The {@linkcode Move} that called this function
*/
windMove(windMove: boolean = true): this { // TODO: is `true` the correct default?
windMove(windMove: boolean = true): this {
this.setFlag(MoveFlags.WIND_MOVE, windMove);
return this;
}
@ -542,7 +549,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.ABSORB}
* @returns The {@linkcode Move} that called this function
*/
triageMove(triageMove: boolean = true): this { // TODO: is `true` the correct default?
triageMove(triageMove: boolean = true): this {
this.setFlag(MoveFlags.TRIAGE_MOVE, triageMove);
return this;
}
@ -553,7 +560,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.SUNSTEEL_STRIKE}
* @returns The {@linkcode Move} that called this function
*/
ignoresAbilities(ignoresAbilities: boolean = true): this { // TODO: is `true` the correct default?
ignoresAbilities(ignoresAbilities: boolean = true): this {
this.setFlag(MoveFlags.IGNORE_ABILITIES, ignoresAbilities);
return this;
}
@ -564,7 +571,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.TRIPLE_AXEL}
* @returns The {@linkcode Move} that called this function
*/
checkAllHits(checkAllHits: boolean = true): this { // TODO: is `true` the correct default?
checkAllHits(checkAllHits: boolean = true): this {
this.setFlag(MoveFlags.CHECK_ALL_HITS, checkAllHits);
return this;
}
@ -575,7 +582,7 @@ export default class Move implements Localizable {
* example: @see {@linkcode Moves.METAL_BURST}
* @returns The {@linkcode Move} that called this function
*/
redirectCounter(redirectCounter: boolean = true): this { // TODO: is `true` the correct default?
redirectCounter(redirectCounter: boolean = true): this {
this.setFlag(MoveFlags.REDIRECT_COUNTER, redirectCounter);
return this;
}
@ -2779,28 +2786,26 @@ export class ResetStatsAttr extends MoveEffectAttr {
super();
this.targetAllPokemon = targetAllPokemon;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
const promises: Promise<void>[] = [];
if (this.targetAllPokemon) { // Target all pokemon on the field when Freezy Frost or Haze are used
const activePokemon = user.scene.getField(true);
activePokemon.forEach(p => this.resetStats(p));
activePokemon.forEach(p => promises.push(this.resetStats(p)));
target.scene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used
this.resetStats(target);
promises.push(this.resetStats(target));
target.scene.queueMessage(i18next.t("moveTriggers:resetStats", {pokemonName: getPokemonNameWithAffix(target)}));
}
await Promise.all(promises);
return true;
}
resetStats(pokemon: Pokemon) {
async resetStats(pokemon: Pokemon): Promise<void> {
for (const s of BATTLE_STATS) {
pokemon.setStatStage(s, 0);
}
pokemon.updateInfo();
return pokemon.updateInfo();
}
}
@ -4747,7 +4752,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
return false;
}
if (move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) {
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) {
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY);
return true;
}
@ -7138,6 +7143,7 @@ export function initMoves() {
new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2),
new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2)
.partial()
.attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL),
new AttackMove(Moves.FALSE_SWIPE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 2)
.attr(SurviveDamageAttr),
@ -7417,9 +7423,11 @@ export function initMoves() {
.attr(HighCritAttr)
.attr(StatusEffectAttr, StatusEffect.BURN),
new StatusMove(Moves.MUD_SPORT, Type.GROUND, -1, 15, -1, 0, 3)
.ignoresProtect()
.attr(AddArenaTagAttr, ArenaTagType.MUD_SPORT, 5)
.target(MoveTarget.BOTH_SIDES),
new AttackMove(Moves.ICE_BALL, Type.ICE, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 3)
.partial()
.attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL)
.ballBombMove(),
new AttackMove(Moves.NEEDLE_ARM, Type.GRASS, MoveCategory.PHYSICAL, 60, 100, 15, 30, 0, 3)
@ -7541,6 +7549,7 @@ export function initMoves() {
.recklessMove(),
new AttackMove(Moves.MAGICAL_LEAF, Type.GRASS, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 3),
new StatusMove(Moves.WATER_SPORT, Type.WATER, -1, 15, -1, 0, 3)
.ignoresProtect()
.attr(AddArenaTagAttr, ArenaTagType.WATER_SPORT, 5)
.target(MoveTarget.BOTH_SIDES),
new SelfStatusMove(Moves.CALM_MIND, Type.PSYCHIC, -1, 20, -1, 0, 3)
@ -7569,6 +7578,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.ROOSTED, true, false)
.triageMove(),
new StatusMove(Moves.GRAVITY, Type.PSYCHIC, -1, 5, -1, 0, 4)
.ignoresProtect()
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
.target(MoveTarget.BOTH_SIDES),
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
@ -8012,7 +8022,15 @@ export function initMoves() {
new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5)
.attr(CopyTypeAttr),
new AttackMove(Moves.RETALIATE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 5, -1, 0, 5)
.partial(),
.attr(MovePowerMultiplierAttr, (user, target, move) => {
const turn = user.scene.currentBattle.turn;
const lastPlayerFaint = user.scene.currentBattle.playerFaintsHistory[user.scene.currentBattle.playerFaintsHistory.length - 1];
const lastEnemyFaint = user.scene.currentBattle.enemyFaintsHistory[user.scene.currentBattle.enemyFaintsHistory.length - 1];
return (
(lastPlayerFaint !== undefined && turn - lastPlayerFaint.turn === 1 && user.isPlayer()) ||
(lastEnemyFaint !== undefined && turn - lastEnemyFaint.turn === 1 && !user.isPlayer())
) ? 2 : 1;
}),
new AttackMove(Moves.FINAL_GAMBIT, Type.FIGHTING, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 5)
.attr(UserHpDamageAttr)
.attr(SacrificialAttrOnHit),

View File

@ -1,8 +1,9 @@
import { PokemonFormChangeItemModifier } from "../modifier/modifier";
import { PokemonFormChangeItemModifier, TerastallizeModifier } from "../modifier/modifier";
import Pokemon from "../field/pokemon";
import { SpeciesFormKey } from "./pokemon-species";
import { StatusEffect } from "./status-effect";
import { MoveCategory, allMoves } from "./move";
import { Type } from "./type";
import { Constructor } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
@ -357,6 +358,41 @@ export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger {
}
}
/**
* Class used for triggering form changes based on the user's Tera type.
* Used by Ogerpon and Terapagos.
* @extends SpeciesFormChangeTrigger
*/
export class SpeciesFormChangeTeraTrigger extends SpeciesFormChangeTrigger {
/** The Tera type that triggers the form change */
private teraType: Type;
constructor(teraType: Type) {
super();
this.teraType = teraType;
}
/**
* Checks if the associated Pokémon has the required Tera Shard that matches with the associated Tera type.
* @param {Pokemon} pokemon the Pokémon that is trying to do the form change
* @returns `true` if the Pokémon can change forms, `false` otherwise
*/
canChange(pokemon: Pokemon): boolean {
return !!pokemon.scene.findModifier(m => m instanceof TerastallizeModifier && m.pokemonId === pokemon.id && m.teraType === this.teraType);
}
}
/**
* Class used for triggering form changes based on the user's lapsed Tera type.
* Used by Ogerpon and Terapagos.
* @extends SpeciesFormChangeTrigger
*/
export class SpeciesFormChangeLapseTeraTrigger extends SpeciesFormChangeTrigger {
canChange(pokemon: Pokemon): boolean {
return !!pokemon.scene.findModifier(m => m instanceof TerastallizeModifier && m.pokemonId === pokemon.id);
}
}
/**
* Class used for triggering form changes based on weather.
* Used by Castform and Cherrim.
@ -592,6 +628,23 @@ export const pokemonFormChanges: PokemonFormChanges = {
[Species.ALTARIA]: [
new SpeciesFormChange(Species.ALTARIA, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.ALTARIANITE))
],
[Species.CASTFORM]: [
new SpeciesFormChange(Species.CASTFORM, "", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
new SpeciesFormChange(Species.CASTFORM, "", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
new SpeciesFormChange(Species.CASTFORM, "", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "", new SpeciesFormChangeActiveTrigger(), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeActiveTrigger(), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeActiveTrigger(), true)
],
[Species.BANETTE]: [
new SpeciesFormChange(Species.BANETTE, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.BANETTITE))
],
@ -627,6 +680,11 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.DEOXYS, "normal", "defense", new SpeciesFormChangeItemTrigger(FormChangeItem.HARD_METEORITE)),
new SpeciesFormChange(Species.DEOXYS, "normal", "speed", new SpeciesFormChangeItemTrigger(FormChangeItem.SMOOTH_METEORITE))
],
[Species.CHERRIM]: [
new SpeciesFormChange(Species.CHERRIM, "overcast", "sunshine", new SpeciesFormChangeWeatherTrigger(Abilities.FLOWER_GIFT, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]), true),
new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]), true),
new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeActiveTrigger(), true)
],
[Species.LOPUNNY]: [
new SpeciesFormChange(Species.LOPUNNY, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.LOPUNNITE))
],
@ -822,6 +880,14 @@ export const pokemonFormChanges: PokemonFormChanges = {
[Species.SANDACONDA]: [
new SpeciesFormChange(Species.SANDACONDA, "", SpeciesFormKey.GIGANTAMAX, new SpeciesFormChangeItemTrigger(FormChangeItem.MAX_MUSHROOMS))
],
[Species.CRAMORANT]: [
new SpeciesFormChange(Species.CRAMORANT, "", "gulping", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() >= .5)),
new SpeciesFormChange(Species.CRAMORANT, "", "gorging", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() < .5)),
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeManualTrigger, true),
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeManualTrigger, true),
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeActiveTrigger(false), true),
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeActiveTrigger(false), true)
],
[Species.TOXTRICITY]: [
new SpeciesFormChange(Species.TOXTRICITY, "amped", SpeciesFormKey.GIGANTAMAX, new SpeciesFormChangeItemTrigger(FormChangeItem.MAX_MUSHROOMS)),
new SpeciesFormChange(Species.TOXTRICITY, "lowkey", SpeciesFormKey.GIGANTAMAX, new SpeciesFormChangeItemTrigger(FormChangeItem.MAX_MUSHROOMS)),
@ -848,6 +914,10 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.ALCREMIE, "caramel-swirl", SpeciesFormKey.GIGANTAMAX, new SpeciesFormChangeItemTrigger(FormChangeItem.MAX_MUSHROOMS)),
new SpeciesFormChange(Species.ALCREMIE, "rainbow-swirl", SpeciesFormKey.GIGANTAMAX, new SpeciesFormChangeItemTrigger(FormChangeItem.MAX_MUSHROOMS))
],
[Species.EISCUE]: [
new SpeciesFormChange(Species.EISCUE, "", "no-ice", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.EISCUE, "no-ice", "", new SpeciesFormChangeManualTrigger(), true)
],
[Species.MORPEKO]: [
new SpeciesFormChange(Species.MORPEKO, "full-belly", "hangry", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.MORPEKO, "hangry", "full-belly", new SpeciesFormChangeManualTrigger(), true)
@ -883,58 +953,24 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.OGERPON, "teal-mask", "wellspring-mask", new SpeciesFormChangeItemTrigger(FormChangeItem.WELLSPRING_MASK)),
new SpeciesFormChange(Species.OGERPON, "teal-mask", "hearthflame-mask", new SpeciesFormChangeItemTrigger(FormChangeItem.HEARTHFLAME_MASK)),
new SpeciesFormChange(Species.OGERPON, "teal-mask", "cornerstone-mask", new SpeciesFormChangeItemTrigger(FormChangeItem.CORNERSTONE_MASK)),
new SpeciesFormChange(Species.OGERPON, "teal-mask", "teal-mask-tera", new SpeciesFormChangeManualTrigger(), true), //When holding a Grass Tera Shard
new SpeciesFormChange(Species.OGERPON, "teal-mask-tera", "teal-mask", new SpeciesFormChangeManualTrigger(), true), //When no longer holding a Grass Tera Shard
new SpeciesFormChange(Species.OGERPON, "wellspring-mask", "wellspring-mask-tera", new SpeciesFormChangeManualTrigger(), true), //When holding a Water Tera Shard
new SpeciesFormChange(Species.OGERPON, "wellspring-mask-tera", "wellspring-mask", new SpeciesFormChangeManualTrigger(), true), //When no longer holding a Water Tera Shard
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask", "hearthflame-mask-tera", new SpeciesFormChangeManualTrigger(), true), //When holding a Fire Tera Shard
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask-tera", "hearthflame-mask", new SpeciesFormChangeManualTrigger(), true), //When no longer holding a Fire Tera Shard
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask", "cornerstone-mask-tera", new SpeciesFormChangeManualTrigger(), true), //When holding a Rock Tera Shard
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask-tera", "cornerstone-mask", new SpeciesFormChangeManualTrigger(), true) //When no longer holding a Rock Tera Shard
new SpeciesFormChange(Species.OGERPON, "teal-mask", "teal-mask-tera", new SpeciesFormChangeTeraTrigger(Type.GRASS)),
new SpeciesFormChange(Species.OGERPON, "teal-mask-tera", "teal-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.GRASS)),
new SpeciesFormChange(Species.OGERPON, "wellspring-mask", "wellspring-mask-tera", new SpeciesFormChangeTeraTrigger(Type.WATER)),
new SpeciesFormChange(Species.OGERPON, "wellspring-mask-tera", "wellspring-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.WATER)),
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask", "hearthflame-mask-tera", new SpeciesFormChangeTeraTrigger(Type.FIRE)),
new SpeciesFormChange(Species.OGERPON, "hearthflame-mask-tera", "hearthflame-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.FIRE)),
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask", "cornerstone-mask-tera", new SpeciesFormChangeTeraTrigger(Type.ROCK)),
new SpeciesFormChange(Species.OGERPON, "cornerstone-mask-tera", "cornerstone-mask", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.ROCK))
],
[Species.TERAPAGOS]: [
new SpeciesFormChange(Species.TERAPAGOS, "", "terastal", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.TERAPAGOS, "terastal", "stellar", new SpeciesFormChangeManualTrigger(), true), //When holding a Stellar Tera Shard
new SpeciesFormChange(Species.TERAPAGOS, "stellar", "terastal", new SpeciesFormChangeManualTrigger(), true) //When no longer holding a Stellar Tera Shard
new SpeciesFormChange(Species.TERAPAGOS, "terastal", "stellar", new SpeciesFormChangeTeraTrigger(Type.STELLAR)),
new SpeciesFormChange(Species.TERAPAGOS, "stellar", "terastal", new SpeciesFormChangeLapseTeraTrigger(), true, new SpeciesFormChangeCondition(p => p.getTeraType() !== Type.STELLAR))
],
[Species.GALAR_DARMANITAN]: [
new SpeciesFormChange(Species.GALAR_DARMANITAN, "", "zen", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.GALAR_DARMANITAN, "zen", "", new SpeciesFormChangeManualTrigger(), true)
],
[Species.EISCUE]: [
new SpeciesFormChange(Species.EISCUE, "", "no-ice", new SpeciesFormChangeManualTrigger(), true),
new SpeciesFormChange(Species.EISCUE, "no-ice", "", new SpeciesFormChangeManualTrigger(), true),
],
[Species.CRAMORANT]: [
new SpeciesFormChange(Species.CRAMORANT, "", "gulping", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() >= .5)),
new SpeciesFormChange(Species.CRAMORANT, "", "gorging", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() < .5)),
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeManualTrigger, true),
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeManualTrigger, true),
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeActiveTrigger(false), true),
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeActiveTrigger(false), true),
],
[Species.CASTFORM]: [
new SpeciesFormChange(Species.CASTFORM, "", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "sunny", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.SUNNY, WeatherType.HARSH_SUN]), true),
new SpeciesFormChange(Species.CASTFORM, "", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "rainy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.RAIN, WeatherType.HEAVY_RAIN]), true),
new SpeciesFormChange(Species.CASTFORM, "", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "snowy", new SpeciesFormChangeWeatherTrigger(Abilities.FORECAST, [WeatherType.HAIL, WeatherType.SNOW]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FORECAST, [WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG]), true),
new SpeciesFormChange(Species.CASTFORM, "sunny", "", new SpeciesFormChangeActiveTrigger(), true),
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeActiveTrigger(), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeActiveTrigger(), true),
],
[Species.CHERRIM]: [
new SpeciesFormChange(Species.CHERRIM, "overcast", "sunshine", new SpeciesFormChangeWeatherTrigger(Abilities.FLOWER_GIFT, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]), true),
new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]), true),
new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeActiveTrigger(), true),
],
};
export function initPokemonForms() {

View File

@ -15,7 +15,7 @@ import { BerryType } from "#enums/berry-type";
import { StatusEffect, getStatusEffectHealText } from "../data/status-effect";
import { achvs } from "../system/achv";
import { VoucherType } from "../system/voucher";
import { FormChangeItem, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms";
import { FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "../data/pokemon-forms";
import { Nature } from "#app/data/nature";
import Overrides from "#app/overrides";
import { ModifierType, modifierTypes } from "./modifier-type";
@ -29,6 +29,7 @@ import { Abilities } from "#app/enums/abilities";
import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { LevelUpPhase } from "#app/phases/level-up-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { SpeciesFormKey } from "#app/data/pokemon-species";
export type ModifierPredicate = (modifier: Modifier) => boolean;
@ -762,6 +763,7 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier {
apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon;
if (pokemon.isPlayer()) {
pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeTeraTrigger);
pokemon.scene.validateAchv(achvs.TERASTALLIZE);
if (this.teraType === Type.STELLAR) {
pokemon.scene.validateAchv(achvs.STELLAR_TERASTALLIZE);
@ -775,6 +777,7 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier {
const ret = super.lapse(args);
if (!ret) {
const pokemon = args[0] as Pokemon;
pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeLapseTeraTrigger);
pokemon.updateSpritePipelineData();
}
return ret;
@ -923,6 +926,18 @@ export class EvolutionStatBoosterModifier extends StatBoosterModifier {
return modifier instanceof EvolutionStatBoosterModifier;
}
/**
* Checks if the stat boosts can apply and if the holder is not currently
* Gigantamax'd.
* @param args [0] {@linkcode Pokemon} that holds the held item
* [1] {@linkcode Stat} N/A
* [2] {@linkcode Utils.NumberHolder} N/A
* @returns true if the stat boosts can be applied, false otherwise
*/
shouldApply(args: any[]): boolean {
return super.shouldApply(args) && ((args[0] as Pokemon).getFormKey() !== SpeciesFormKey.GIGANTAMAX);
}
/**
* Boosts the incoming stat value by a {@linkcode multiplier} if the holder
* can evolve. Note that, if the holder is a fusion, they will receive

View File

@ -55,8 +55,10 @@ export class FaintPhase extends PokemonPhase {
// Track total times pokemon have been KO'd for supreme overlord/last respects
if (pokemon.isPlayer()) {
this.scene.currentBattle.playerFaints += 1;
this.scene.currentBattle.playerFaintsHistory.push({ pokemon: pokemon, turn: this.scene.currentBattle.turn });
} else {
this.scene.currentBattle.enemyFaints += 1;
this.scene.currentBattle.enemyFaintsHistory.push({ pokemon: pokemon, turn: this.scene.currentBattle.turn });
}
this.scene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true);

View File

@ -3,7 +3,7 @@ import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, ProtectedTag, SemiInvulnerableTag } from "#app/data/battler-tags";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
@ -152,7 +152,8 @@ export class MoveEffectPhase extends PokemonPhase {
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value || target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)));
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Does this phase represent the invoked move's first strike? */
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);

View File

@ -1,16 +1,15 @@
import { Stat } from "#enums/stat";
import { EvolutionStatBoosterModifier } from "#app/modifier/modifier";
import { modifierTypes } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
import * as Utils from "#app/utils";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phase from "phaser";
import * as Utils from "#app/utils";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { StatBoosterModifier } from "#app/modifier/modifier";
describe("Items - Eviolite", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phase.Game({
@ -25,108 +24,65 @@ describe("Items - Eviolite", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override
.battleType("single")
.startingHeldItems([{ name: "EVIOLITE" }]);
});
it("EVIOLITE activates in battle correctly", async() => {
game.override.startingHeldItems([{ name: "EVIOLITE" }]);
const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([
it("should provide 50% boost to DEF and SPDEF for unevolved, unfused pokemon", async() => {
await game.classicMode.startBattle([
Species.PICHU
]);
const partyMember = game.scene.getParty()[0];
const partyMember = game.scene.getPlayerPokemon()!;
// Checking console log to make sure Eviolite is applied when getEffectiveStat (with the appropriate stat) is called
partyMember.getEffectiveStat(Stat.DEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
console.log("");
// Ignore other calculations for simplicity
partyMember.getEffectiveStat(Stat.SPDEF);
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log("");
partyMember.getEffectiveStat(Stat.ATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log("");
partyMember.getEffectiveStat(Stat.SPATK);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
console.log("");
partyMember.getEffectiveStat(Stat.SPD);
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:ModifierType.EVIOLITE.name"), "");
return Math.floor(statValue.value);
});
it("EVIOLITE held by unevolved, unfused pokemon", async() => {
await game.startBattle([
Species.PICHU
]);
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
const partyMember = game.scene.getParty()[0];
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.5));
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.5));
}, TIMEOUT);
const defStat = partyMember.getStat(Stat.DEF);
const spDefStat = partyMember.getStat(Stat.SPDEF);
// Making sure modifier is not applied without holding item
const defValue = new Utils.NumberHolder(defStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
const spDefValue = new Utils.NumberHolder(spDefStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
// Giving Eviolite to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.EVIOLITE().newModifier(partyMember), true);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
expect(defValue.value / defStat).toBe(1.5);
expect(spDefValue.value / spDefStat).toBe(1.5);
}, 20000);
it("EVIOLITE held by fully evolved, unfused pokemon", async() => {
await game.startBattle([
it("should not provide a boost for fully evolved, unfused pokemon", async() => {
await game.classicMode.startBattle([
Species.RAICHU,
]);
const partyMember = game.scene.getParty()[0];
const defStat = partyMember.getStat(Stat.DEF);
const spDefStat = partyMember.getStat(Stat.SPDEF);
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Making sure modifier is not applied without holding item
const defValue = new Utils.NumberHolder(defStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
const spDefValue = new Utils.NumberHolder(spDefStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
// Ignore other calculations for simplicity
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
return Math.floor(statValue.value);
});
// Giving Eviolite to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.EVIOLITE().newModifier(partyMember), true);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
}, 20000);
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(defStat);
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(spDefStat);
it("EVIOLITE held by completely unevolved, fused pokemon", async() => {
await game.startBattle([
}, TIMEOUT);
it("should provide 50% boost to DEF and SPDEF for completely unevolved, fused pokemon", async() => {
await game.classicMode.startBattle([
Species.PICHU,
Species.CLEFFA
]);
const partyMember = game.scene.getParty()[0];
const ally = game.scene.getParty()[1];
const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@ -137,35 +93,29 @@ describe("Items - Eviolite", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
const defStat = partyMember.getStat(Stat.DEF);
const spDefStat = partyMember.getStat(Stat.SPDEF);
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Making sure modifier is not applied without holding item
const defValue = new Utils.NumberHolder(defStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
const spDefValue = new Utils.NumberHolder(spDefStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
// Ignore other calculations for simplicity
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
return Math.floor(statValue.value);
});
// Giving Eviolite to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.EVIOLITE().newModifier(partyMember), true);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
expect(defValue.value / defStat).toBe(1.5);
expect(spDefValue.value / spDefStat).toBe(1.5);
}, 20000);
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.5));
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.5));
}, TIMEOUT);
it("EVIOLITE held by partially unevolved (base), fused pokemon", async() => {
await game.startBattle([
it("should provide 25% boost to DEF and SPDEF for partially unevolved (base), fused pokemon", async() => {
await game.classicMode.startBattle([
Species.PICHU,
Species.CLEFABLE
]);
const partyMember = game.scene.getParty()[0];
const ally = game.scene.getParty()[1];
const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@ -176,35 +126,29 @@ describe("Items - Eviolite", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
const defStat = partyMember.getStat(Stat.DEF);
const spDefStat = partyMember.getStat(Stat.SPDEF);
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Making sure modifier is not applied without holding item
const defValue = new Utils.NumberHolder(defStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
const spDefValue = new Utils.NumberHolder(spDefStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
// Ignore other calculations for simplicity
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
return Math.floor(statValue.value);
});
// Giving Eviolite to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.EVIOLITE().newModifier(partyMember), true);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
expect(defValue.value / defStat).toBe(1.25);
expect(spDefValue.value / spDefStat).toBe(1.25);
}, 20000);
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.25));
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.25));
}, TIMEOUT);
it("EVIOLITE held by partially unevolved (fusion), fused pokemon", async() => {
await game.startBattle([
it("should provide 25% boost to DEF and SPDEF for partially unevolved (fusion), fused pokemon", async() => {
await game.classicMode.startBattle([
Species.RAICHU,
Species.CLEFFA
]);
const partyMember = game.scene.getParty()[0];
const ally = game.scene.getParty()[1];
const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@ -215,35 +159,29 @@ describe("Items - Eviolite", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
const defStat = partyMember.getStat(Stat.DEF);
const spDefStat = partyMember.getStat(Stat.SPDEF);
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Making sure modifier is not applied without holding item
const defValue = new Utils.NumberHolder(defStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
const spDefValue = new Utils.NumberHolder(spDefStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
// Ignore other calculations for simplicity
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
return Math.floor(statValue.value);
});
// Giving Eviolite to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.EVIOLITE().newModifier(partyMember), true);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
expect(defValue.value / defStat).toBe(1.25);
expect(spDefValue.value / spDefStat).toBe(1.25);
}, 20000);
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(Math.floor(defStat * 1.25));
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(Math.floor(spDefStat * 1.25));
}, TIMEOUT);
it("EVIOLITE held by completely evolved, fused pokemon", async() => {
await game.startBattle([
it("should not provide a boost for fully evolved, fused pokemon", async() => {
await game.classicMode.startBattle([
Species.RAICHU,
Species.CLEFABLE
]);
const partyMember = game.scene.getParty()[0];
const ally = game.scene.getParty()[1];
const [ partyMember, ally ] = game.scene.getParty();
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
partyMember.fusionSpecies = ally.species;
@ -254,24 +192,51 @@ describe("Items - Eviolite", () => {
partyMember.fusionGender = ally.gender;
partyMember.fusionLuck = ally.luck;
const defStat = partyMember.getStat(Stat.DEF);
const spDefStat = partyMember.getStat(Stat.SPDEF);
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Making sure modifier is not applied without holding item
const defValue = new Utils.NumberHolder(defStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
const spDefValue = new Utils.NumberHolder(spDefStat);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
// Ignore other calculations for simplicity
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
return Math.floor(statValue.value);
});
// Giving Eviolite to party member and testing if it applies
partyMember.scene.addModifier(modifierTypes.EVIOLITE().newModifier(partyMember), true);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
partyMember.scene.applyModifiers(EvolutionStatBoosterModifier, true, partyMember, Stat.SPDEF, spDefValue);
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
expect(defValue.value / defStat).toBe(1);
expect(spDefValue.value / spDefStat).toBe(1);
}, 20000);
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(defStat);
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(spDefStat);
}, TIMEOUT);
it("should not provide a boost for Gigantamax Pokémon", async() => {
game.override.starterForms({
[Species.PIKACHU]: 8,
[Species.EEVEE]: 2,
[Species.DURALUDON]: 1,
[Species.MEOWTH]: 1
});
const gMaxablePokemon = [ Species.PIKACHU, Species.EEVEE, Species.DURALUDON, Species.MEOWTH ];
await game.classicMode.startBattle([
Utils.randItem(gMaxablePokemon)
]);
const partyMember = game.scene.getPlayerPokemon()!;
vi.spyOn(partyMember, "getEffectiveStat").mockImplementation((stat, _opponent?, _move?, _isCritical?) => {
const statValue = new Utils.NumberHolder(partyMember.getStat(stat, false));
game.scene.applyModifiers(StatBoosterModifier, partyMember.isPlayer(), partyMember, stat, statValue);
// Ignore other calculations for simplicity
return Math.floor(statValue.value);
});
const defStat = partyMember.getStat(Stat.DEF, false);
const spDefStat = partyMember.getStat(Stat.SPDEF, false);
expect(partyMember.getEffectiveStat(Stat.DEF)).toBe(defStat);
expect(partyMember.getEffectiveStat(Stat.SPDEF)).toBe(spDefStat);
}, TIMEOUT);
});

View File

@ -0,0 +1,43 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Baddy Bad", () => {
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
.moveset([Moves.SPLASH])
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.ability(Abilities.BALL_FETCH);
});
it("should not activate Reflect if the move fails due to Protect", async () => {
game.override.enemyMoveset(Moves.PROTECT);
await game.classicMode.startBattle([Species.FEEBAS]);
game.move.select(Moves.BADDY_BAD);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.tags.length).toBe(0);
}, TIMEOUT);
});

View File

@ -1,12 +1,12 @@
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
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";
import { allMoves } from "#app/data/move";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { CommandPhase } from "#app/phases/command-phase";
describe("Moves - Freezy Frost", () => {
let phaserGame: Phaser.Game;
@ -23,38 +23,83 @@ describe("Moves - Freezy Frost", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override
.battleType("single")
.enemySpecies(Species.RATTATA)
.enemyLevel(100)
.enemyMoveset([ Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL ])
.enemyAbility(Abilities.BALL_FETCH)
.startingLevel(100)
.moveset([ Moves.FREEZY_FROST, Moves.HOWL, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH);
game.override.enemySpecies(Species.RATTATA);
game.override.enemyLevel(100);
game.override.enemyMoveset(Moves.SPLASH);
game.override.enemyAbility(Abilities.NONE);
game.override.startingLevel(100);
game.override.moveset([Moves.FREEZY_FROST, Moves.SWORDS_DANCE, Moves.CHARM, Moves.SPLASH]);
vi.spyOn(allMoves[Moves.FREEZY_FROST], "accuracy", "get").mockReturnValue(100);
game.override.ability(Abilities.NONE);
vi.spyOn(allMoves[ Moves.FREEZY_FROST ], "accuracy", "get").mockReturnValue(100);
});
it("should clear all stat stage changes", { timeout: 10000 }, async () => {
await game.startBattle([Species.RATTATA]);
it(
"should clear stat changes of user and opponent",
async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const user = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(user.getStatStage(Stat.ATK)).toBe(0);
expect(enemy.getStatStage(Stat.ATK)).toBe(0);
game.move.select(Moves.HOWL);
await game.toNextTurn();
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to(TurnInitPhase);
game.move.select(Moves.CHARM);
await game.phaseInterceptor.to(TurnInitPhase);
expect(user.getStatStage(Stat.ATK)).toBe(2);
expect(enemy.getStatStage(Stat.ATK)).toBe(-2);
expect(user.getStatStage(Stat.ATK)).toBe(1);
expect(enemy.getStatStage(Stat.ATK)).toBe(1);
game.move.select(Moves.FREEZY_FROST);
await game.phaseInterceptor.to(TurnInitPhase);
await game.toNextTurn();
expect(user.getStatStage(Stat.ATK)).toBe(0);
expect(enemy.getStatStage(Stat.ATK)).toBe(0);
});
it(
"should clear all stat changes even when enemy uses the move",
async () => {
game.override.enemyMoveset([ Moves.FREEZY_FROST, Moves.FREEZY_FROST, Moves.FREEZY_FROST, Moves.FREEZY_FROST ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]); // Shuckle for slower Howl on first turn so Freezy Frost doesn't affect it.
const user = game.scene.getPlayerPokemon()!;
game.move.select(Moves.HOWL);
await game.toNextTurn();
const userAtkBefore = user.getStatStage(Stat.ATK);
expect(userAtkBefore).toBe(1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(user.getStatStage(Stat.ATK)).toBe(0);
});
it(
"should clear all stat changes in double battle",
async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.SHUCKLE, Species.RATTATA ]);
const [ leftPlayer, rightPlayer ] = game.scene.getPlayerField();
const [ leftOpp, rightOpp ] = game.scene.getEnemyField();
game.move.select(Moves.HOWL, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(leftPlayer.getStatStage(Stat.ATK)).toBe(1);
expect(rightPlayer.getStatStage(Stat.ATK)).toBe(1);
expect(leftOpp.getStatStage(Stat.ATK)).toBe(2); // Both enemies use Howl
expect(rightOpp.getStatStage(Stat.ATK)).toBe(2);
game.move.select(Moves.FREEZY_FROST, 0, leftOpp.getBattlerIndex());
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(leftPlayer.getStatStage(Stat.ATK)).toBe(0);
expect(rightPlayer.getStatStage(Stat.ATK)).toBe(0);
expect(leftOpp.getStatStage(Stat.ATK)).toBe(0);
expect(rightOpp.getStatStage(Stat.ATK)).toBe(0);
});
});

View File

@ -0,0 +1,71 @@
import { Moves } from "#app/enums/moves";
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Obstruct", () => {
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("single")
.enemyAbility(Abilities.BALL_FETCH)
.ability(Abilities.BALL_FETCH)
.moveset([Moves.OBSTRUCT]);
});
it("protects from contact damaging moves and lowers the opponent's defense by 2 stages", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.ICE_PUNCH));
await game.classicMode.startBattle();
game.move.select(Moves.OBSTRUCT);
await game.phaseInterceptor.to("BerryPhase");
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(player.isFullHp()).toBe(true);
expect(enemy.getStatStage(Stat.DEF)).toBe(-2);
}, TIMEOUT);
it("protects from non-contact damaging moves and doesn't lower the opponent's defense by 2 stages", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.WATER_GUN));
await game.classicMode.startBattle();
game.move.select(Moves.OBSTRUCT);
await game.phaseInterceptor.to("BerryPhase");
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
expect(player.isFullHp()).toBe(true);
expect(enemy.getStatStage(Stat.DEF)).toBe(0);
}, TIMEOUT);
it("doesn't protect from status moves", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.GROWL));
await game.classicMode.startBattle();
game.move.select(Moves.OBSTRUCT);
await game.phaseInterceptor.to("BerryPhase");
const player = game.scene.getPlayerPokemon()!;
expect(player.getStatStage(Stat.ATK)).toBe(-1);
}, TIMEOUT);
});

View File

@ -0,0 +1,49 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { allMoves } from "#app/data/move";
describe("Moves - Retaliate", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const retaliate = allMoves[Moves.RETALIATE];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.SNORLAX)
.enemyMoveset([Moves.RETALIATE, Moves.RETALIATE, Moves.RETALIATE, Moves.RETALIATE])
.enemyLevel(100)
.moveset([Moves.RETALIATE, Moves.SPLASH])
.startingLevel(80)
.disableCrits();
});
it("increases power if ally died previous turn", async () => {
vi.spyOn(retaliate, "calculateBattlePower");
await game.startBattle([Species.ABRA, Species.COBALION]);
game.move.select(Moves.RETALIATE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(retaliate.calculateBattlePower).toHaveLastReturnedWith(70);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.RETALIATE);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(retaliate.calculateBattlePower).toHaveReturnedWith(140);
});
});

View File

@ -541,7 +541,9 @@ export default class RunInfoUiHandler extends UiHandler {
// Contains Name, Level + Nature, Ability, Passive
const pokeInfoTextContainer = this.scene.add.container(-85, 3.5);
const textContainerFontSize = "34px";
const pNature = getNatureName(pokemon.nature);
// This checks if the Pokemon's nature has been overwritten during the run and displays the change accurately
const pNature = pokemon.getNature();
const pNatureName = getNatureName(pNature);
const pName = pokemon.getNameToRender();
//With the exception of Korean/Traditional Chinese/Simplified Chinese, the code shortens the terms for ability and passive to their first letter.
//These languages are exempted because they are already short enough.
@ -557,7 +559,7 @@ export default class RunInfoUiHandler extends UiHandler {
// Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12.
const lineSpacing = (i18next.resolvedLanguage === "ja") ? 12 : 3;
const pokeInfoText = addBBCodeTextObject(this.scene, 0, 0, pName, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing: lineSpacing});
pokeInfoText.appendText(`${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatFancyLargeNumber(pokemon.level, 1)} - ${pNature}`);
pokeInfoText.appendText(`${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatFancyLargeNumber(pokemon.level, 1)} - ${pNatureName}`);
pokeInfoText.appendText(pAbilityInfo);
pokeInfoText.appendText(pPassiveInfo);
pokeInfoTextContainer.add(pokeInfoText);
@ -568,7 +570,7 @@ export default class RunInfoUiHandler extends UiHandler {
const pStats : string[]= [];
pokemon.stats.forEach((element) => pStats.push(Utils.formatFancyLargeNumber(element, 1)));
for (let i = 0; i < pStats.length; i++) {
const isMult = getNatureStatMultiplier(pokemon.nature, i);
const isMult = getNatureStatMultiplier(pNature, i);
pStats[i] = (isMult < 1) ? pStats[i] + "[color=#40c8f8]↓[/color]" : pStats[i];
pStats[i] = (isMult > 1) ? pStats[i] + "[color=#f89890]↑[/color]" : pStats[i];
}
@ -889,11 +891,13 @@ export default class RunInfoUiHandler extends UiHandler {
}
break;
case Button.CYCLE_ABILITY:
if (this.runInfo.modifiers.length !== 0) {
if (this.partyVisibility) {
this.showParty(false);
} else {
this.showParty(true);
}
}
break;
}
}

View File

@ -28,15 +28,15 @@ import { Mode } from "./ui";
import { addWindow } from "./ui-theme";
import { Egg } from "#app/data/egg";
import Overrides from "#app/overrides";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
import {Passive as PassiveAttr} from "#enums/passive";
import { SettingKeyboard } from "#app/system/settings/settings-keyboard";
import { Passive as PassiveAttr } from "#enums/passive";
import * as Challenge from "../data/challenge";
import MoveInfoOverlay from "./move-info-overlay";
import { getEggTierForSpecies } from "#app/data/egg";
import { Device } from "#enums/devices";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import {Button} from "#enums/buttons";
import { Button } from "#enums/buttons";
import { EggSourceType } from "#app/enums/egg-source-types";
import AwaitableUiHandler from "./awaitable-ui-handler";
import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "./dropdown";
@ -2905,7 +2905,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
const speciesForm = getPokemonSpeciesForm(species.speciesId, props.formIndex);
this.setTypeIcons(speciesForm.type1, speciesForm!.type2!); // TODO: are those bangs correct?
this.setTypeIcons(speciesForm.type1, speciesForm.type2);
this.pokemonSprite.clearTint();
if (this.pokerusSpecies.includes(species)) {
@ -3242,13 +3242,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonFormText.setText(formText ? i18next.t(`pokemonForm:${speciesName}${formText}`) : "");
}
this.setTypeIcons(speciesForm.type1, speciesForm.type2!); // TODO: is this bang correct?
this.setTypeIcons(speciesForm.type1, speciesForm.type2);
} else {
this.pokemonAbilityText.setText("");
this.pokemonPassiveText.setText("");
this.pokemonNatureText.setText("");
// @ts-ignore
this.setTypeIcons(null, null); // TODO: resolve ts-ignore.. huh!?
this.setTypeIcons(null, null);
}
} else {
this.shinyOverlay.setVisible(false);
@ -3258,8 +3257,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonAbilityText.setText("");
this.pokemonPassiveText.setText("");
this.pokemonNatureText.setText("");
// @ts-ignore
this.setTypeIcons(null, null); // TODO: resolve ts-ignore.. huh!?
this.setTypeIcons(null, null);
}
if (!this.starterMoveset) {
@ -3292,7 +3290,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.updateInstructions();
}
setTypeIcons(type1: Type, type2: Type): void {
setTypeIcons(type1: Type | null, type2: Type | null): void {
if (type1 !== null) {
this.type1Icon.setVisible(true);
this.type1Icon.setFrame(Type[type1].toLowerCase());