Merge beta and resolve conflicts

This commit is contained in:
Christopher Schmidt 2024-10-26 11:50:43 -04:00
commit df04e31886
99 changed files with 4277 additions and 1512 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "future_self_f.png",
"format": "RGBA8888",
"size": {
"w": 29,
"h": 69
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 69,
"h": 69
},
"spriteSourceSize": {
"x": 21,
"y": 0,
"w": 29,
"h": 69
},
"frame": {
"x": 0,
"y": 0,
"w": 29,
"h": 69
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:4eb16332c2e77886e4e621b62269f05e:26f1bc53c853efdbe228d67604b95b54:d25525a5db42bd57d2afe4b6e3081ee1$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "future_self_m.png",
"format": "RGBA8888",
"size": {
"w": 36,
"h": 73
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 73,
"h": 73
},
"spriteSourceSize": {
"x": 18,
"y": 0,
"w": 36,
"h": 73
},
"frame": {
"x": 0,
"y": 0,
"w": 36,
"h": 73
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:0d8d68d06ae75bc93d72b54183a9df02:d3d509801da9ff5c0bd4793a05ece391:83c4b8c2ed25ea7d9795bec5c40c8602$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

@ -1 +1 @@
Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef
Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f

View File

@ -1387,7 +1387,7 @@ export default class BattleScene extends SceneBase {
case Species.ZYGARDE:
return Utils.randSeedInt(4);
case Species.MINIOR:
return Utils.randSeedInt(6);
return Utils.randSeedInt(7);
case Species.ALCREMIE:
return Utils.randSeedInt(9);
case Species.MEOWSTIC:

View File

@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain";
@ -1139,7 +1139,9 @@ export class MoveEffectChanceMultiplierAbAttr extends AbAttr {
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
// Disable showAbility during getTargetBenefitScore
this.showAbility = args[4];
if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) {
const exceptMoves = [ Moves.ORDER_UP, Moves.ELECTRO_SHOT ];
if ((args[0] as Utils.NumberHolder).value <= 0 || exceptMoves.includes((args[1] as Move).id)) {
return false;
}
@ -1329,7 +1331,6 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
*/
const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr,
ChargeAttr,
SacrificialAttr,
SacrificialAttrOnHit
];
@ -1345,6 +1346,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
/** Also check if this move is an Attack move and if it's only targeting one Pokemon */
return numTargets === 1
&& !move.isChargingMove()
&& !exceptAttrs.some(attr => move.hasAttr(attr))
&& !exceptMoves.some(id => move.id === id)
&& move.category !== MoveCategory.STATUS;
@ -2420,11 +2422,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
super(true);
}
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise<boolean> {
const targets = pokemon.getOpponents();
if (simulated || !targets.length) {
return simulated;
}
const promises: Promise<void>[] = [];
let target: Pokemon;
if (targets.length > 1) {
@ -2433,7 +2436,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
target = targets[0];
}
target = target!; // compiler doesn't know its guranteed to be defined
target = target!;
pokemon.summonData.speciesForm = target.getSpeciesForm();
pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
pokemon.summonData.ability = target.getAbility().id;
@ -2450,18 +2453,26 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
pokemon.setStatStage(s, target.getStatStage(s));
}
pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId ?? Moves.NONE, m?.ppUsed, m?.ppUp));
pokemon.summonData.types = target.getTypes();
pokemon.scene.playSound("battle_anims/PRSFX- Transform");
pokemon.loadAssets(false).then(() => {
pokemon.playAnim();
pokemon.updateInfo();
pokemon.summonData.moveset = target.getMoveset().map((m) => {
if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
} else {
console.warn(`Imposter: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(Moves.NONE);
}
});
pokemon.summonData.types = target.getTypes();
promises.push(pokemon.updateInfo());
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, }));
pokemon.scene.playSound("battle_anims/PRSFX- Transform");
promises.push(pokemon.loadAssets(false).then(() => {
pokemon.playAnim();
pokemon.updateInfo();
}));
await Promise.all(promises);
return true;
}
@ -4200,6 +4211,11 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr {
export class BlockRedirectAbAttr extends AbAttr { }
/**
* Used by Early Bird, makes the pokemon wake up faster
* @param statusEffect - The {@linkcode StatusEffect} to check for
* @see {@linkcode apply}
*/
export class ReduceStatusEffectDurationAbAttr extends AbAttr {
private statusEffect: StatusEffect;
@ -4209,9 +4225,19 @@ export class ReduceStatusEffectDurationAbAttr extends AbAttr {
this.statusEffect = statusEffect;
}
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
/**
* Reduces the number of sleep turns remaining by an extra 1 when applied
* @param args - The args passed to the `AbAttr`:
* - `[0]` - The {@linkcode StatusEffect} of the Pokemon
* - `[1]` - The number of turns remaining until the status is healed
* @returns `true` if the ability was applied
*/
apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!(args[1] instanceof Utils.NumberHolder)) {
return false;
}
if (args[0] === this.statusEffect) {
(args[1] as Utils.IntegerHolder).value = Utils.toDmgValue((args[1] as Utils.IntegerHolder).value / 2);
args[1].value -= 1;
return true;
}
@ -4342,6 +4368,30 @@ export class AlwaysHitAbAttr extends AbAttr { }
/** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
export class IgnoreProtectOnContactAbAttr extends AbAttr { }
/**
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Infiltrator_(Ability) | Infiltrator}.
* Allows the source's moves to bypass the effects of opposing Light Screen, Reflect, Aurora Veil, Safeguard, Mist, and Substitute.
*/
export class InfiltratorAbAttr extends AbAttr {
/**
* Sets a flag to bypass screens, Substitute, Safeguard, and Mist
* @param pokemon n/a
* @param passive n/a
* @param simulated n/a
* @param cancelled n/a
* @param args `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} containing the flag
* @returns `true` if the bypass flag was successfully set; `false` otherwise.
*/
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean {
const bypassed = args[0];
if (args[0] instanceof Utils.BooleanHolder) {
bypassed.value = true;
return true;
}
return false;
}
}
export class UncopiableAbilityAbAttr extends AbAttr {
constructor() {
super(false);
@ -5321,7 +5371,8 @@ export function initAbilities() {
.attr(PostSummonTransformAbAttr)
.attr(UncopiableAbilityAbAttr),
new Ability(Abilities.INFILTRATOR, 5)
.unimplemented(),
.attr(InfiltratorAbAttr)
.partial(), // does not bypass Mist
new Ability(Abilities.MUMMY, 5)
.attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY)
.bypassFaint(),

View File

@ -7,7 +7,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon";
import { StatusEffect } from "#app/data/status-effect";
import { BattlerIndex } from "#app/battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, InfiltratorAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability";
import { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
import i18next from "i18next";
@ -130,7 +130,18 @@ export class MistTag extends ArenaTag {
* to flag the stat reduction as cancelled
* @returns `true` if a stat reduction was cancelled; `false` otherwise
*/
override apply(arena: Arena, simulated: boolean, cancelled: BooleanHolder): boolean {
override apply(arena: Arena, simulated: boolean, attacker: Pokemon, cancelled: BooleanHolder): boolean {
// `StatStageChangePhase` currently doesn't have a reference to the source of stat drops,
// so this code currently has no effect on gameplay.
if (attacker) {
const bypassed = new BooleanHolder(false);
// TODO: Allow this to be simulated
applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed);
if (bypassed.value) {
return false;
}
}
cancelled.value = true;
if (!simulated) {
@ -169,12 +180,18 @@ export class WeakenMoveScreenTag extends ArenaTag {
*
* @param arena the {@linkcode Arena} where the move is applied.
* @param simulated n/a
* @param attacker the attacking {@linkcode Pokemon}
* @param moveCategory the attacking move's {@linkcode MoveCategory}.
* @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier
* @returns `true` if the attacking move was weakened; `false` otherwise.
*/
override apply(arena: Arena, simulated: boolean, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean {
override apply(arena: Arena, simulated: boolean, attacker: Pokemon, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean {
if (this.weakenedCategories.includes(moveCategory)) {
const bypassed = new BooleanHolder(false);
applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed);
if (bypassed.value) {
return false;
}
damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5;
return true;
}
@ -953,6 +970,9 @@ export class GravityTag extends ArenaTag {
if (pokemon !== null) {
pokemon.removeTag(BattlerTagType.FLOATING);
pokemon.removeTag(BattlerTagType.TELEKINESIS);
if (pokemon.getTag(BattlerTagType.FLYING)) {
pokemon.addTag(BattlerTagType.INTERRUPTED);
}
}
});
}

View File

@ -1,7 +1,6 @@
import { Gender } from "#app/data/gender";
import { PokeballType } from "#app/data/pokeball";
import Pokemon from "#app/field/pokemon";
import { Stat } from "#enums/stat";
import { Type } from "#app/data/type";
import * as Utils from "#app/utils";
import { WeatherType } from "#app/data/weather";
@ -10,7 +9,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day";
import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier";
import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, TempExtraModifierModifier } from "#app/modifier/modifier";
import { SpeciesFormKey } from "#enums/species-form-key";
@ -271,9 +270,21 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY))
],
[Species.TYROGUE]: [
new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] > p.stats[Stat.DEF])),
new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] < p.stats[Stat.DEF])),
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF]))
/**
* Custom: Evolves into Hitmonlee, Hitmonchan or Hitmontop at level 20
* if it knows Low Sweep, Mach Punch, or Rapid Spin, respectively.
* If Tyrogue knows multiple of these moves, its evolution is based on
* the first qualifying move in its moveset.
*/
new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p =>
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.LOW_SWEEP
)),
new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p =>
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.MACH_PUNCH
)),
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p =>
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.RAPID_SPIN
)),
],
[Species.KOFFING]: [
new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)),
@ -1652,11 +1663,11 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG),
|| m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG)
|| m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG)
]
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
//import { battleAnimRawData } from "./battle-anim-raw-data";
import BattleScene from "../battle-scene";
import { AttackMove, BeakBlastHeaderAttr, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
import Pokemon from "../field/pokemon";
import * as Utils from "../utils";
import { BattlerIndex } from "../battle";
@ -476,8 +476,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else {
const loadedCheckTimer = setInterval(() => {
if (moveAnims.get(move) !== null) {
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0];
if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) {
const chargeAnimSource = (allMoves[move].isChargingMove())
? allMoves[move]
: (allMoves[move].getAttrs(DelayedAttackAttr)[0]
?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) {
return;
}
clearInterval(loadedCheckTimer);
@ -507,11 +510,12 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else {
populateMoveAnim(move, ba);
}
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0]
|| allMoves[move].getAttrs(DelayedAttackAttr)[0]
|| allMoves[move].getAttrs(BeakBlastHeaderAttr)[0];
if (chargeAttr) {
initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve());
const chargeAnimSource = (allMoves[move].isChargingMove())
? allMoves[move]
: (allMoves[move].getAttrs(DelayedAttackAttr)[0]
?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
if (chargeAnimSource) {
initMoveChargeAnim(scene, chargeAnimSource.chargeAnim).then(() => resolve());
} else {
resolve();
}
@ -638,11 +642,12 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo
return new Promise(resolve => {
const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat();
for (const moveId of moveIds) {
const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0]
|| allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
|| allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0];
if (chargeAttr) {
const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim);
const chargeAnimSource = (allMoves[moveId].isChargingMove())
? allMoves[moveId]
: (allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]);
if (chargeAnimSource) {
const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim);
moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct?
if (Array.isArray(moveChargeAnims)) {
moveAnimations.push(moveChargeAnims[1]);

View File

@ -1,29 +1,43 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
import { StatusEffect } from "./status-effect";
import * as Utils from "../utils";
import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move";
import { Type } from "./type";
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability";
import { TerrainType } from "./terrain";
import { WeatherType } from "./weather";
import { allAbilities } from "./ability";
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import i18next from "#app/plugins/i18n";
import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat";
import BattleScene from "#app/battle-scene";
import {
allAbilities,
applyAbAttrs,
BlockNonDirectDamageAbAttr,
FlinchEffectAbAttr,
ProtectStatAbAttr,
ReverseDrainAbAttr
} from "#app/data/ability";
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims";
import Move, {
allMoves,
applyMoveAttrs,
ConsecutiveUseDoublePowerAttr,
HealOnAllyAttr,
MoveCategory,
MoveFlags,
StatusCategoryOnAllyAttr
} from "#app/data/move";
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
import { StatusEffect } from "#app/data/status-effect";
import { TerrainType } from "#app/data/terrain";
import { Type } from "#app/data/type";
import { WeatherType } from "#app/data/weather";
import Pokemon, { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
import BattleScene from "#app/battle-scene";
import { StatStageChangeCallback, StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import i18next from "#app/plugins/i18n";
import { BooleanHolder, getFrameMs, NumberHolder, toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { Species } from "#enums/species";
import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat } from "#enums/stat";
export enum BattlerTagLapseType {
FAINT,
@ -33,6 +47,7 @@ export enum BattlerTagLapseType {
MOVE_EFFECT,
TURN_END,
HIT,
AFTER_HIT,
CUSTOM
}
@ -405,7 +420,7 @@ export class RechargingTag extends BattlerTag {
*/
export class BeakBlastChargingTag extends BattlerTag {
constructor() {
super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 1, Moves.BEAK_BLAST);
super(BattlerTagType.BEAK_BLAST_CHARGING, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1, Moves.BEAK_BLAST);
}
onAdd(pokemon: Pokemon): void {
@ -421,16 +436,13 @@ export class BeakBlastChargingTag extends BattlerTag {
* to be removed after the source makes a move (or the turn ends, whichever comes first)
* @param pokemon {@linkcode Pokemon} the owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle
* @returns `true` if invoked with the CUSTOM lapse type; `false` otherwise
* @returns `true` if invoked with the `AFTER_HIT` lapse type
*/
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) {
const effectPhase = pokemon.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase) {
const attacker = effectPhase.getPokemon();
if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
}
if (lapseType === BattlerTagLapseType.AFTER_HIT) {
const phaseData = getMoveEffectPhaseData(pokemon);
if (phaseData?.move.hasFlag(MoveFlags.MAKES_CONTACT)) {
phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
}
return true;
}
@ -444,11 +456,10 @@ export class BeakBlastChargingTag extends BattlerTag {
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap}
*/
export class ShellTrapTag extends BattlerTag {
public activated: boolean;
public activated: boolean = false;
constructor() {
super(BattlerTagType.SHELL_TRAP, BattlerTagLapseType.TURN_END, 1);
this.activated = false;
super(BattlerTagType.SHELL_TRAP, [ BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1);
}
onAdd(pokemon: Pokemon): void {
@ -459,25 +470,33 @@ export class ShellTrapTag extends BattlerTag {
* "Activates" the shell trap, causing the tag owner to move next.
* @param pokemon {@linkcode Pokemon} the owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle
* @returns `true` if invoked with the `CUSTOM` lapse type; `false` otherwise
* @returns `true` if invoked with the `AFTER_HIT` lapse type
*/
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) {
const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex(
phase => phase instanceof MovePhase && phase.pokemon === pokemon
);
const firstMovePhaseIndex = pokemon.scene.phaseQueue.findIndex(
phase => phase instanceof MovePhase
);
if (lapseType === BattlerTagLapseType.AFTER_HIT) {
const phaseData = getMoveEffectPhaseData(pokemon);
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase);
// Trap should only be triggered by opponent's Physical moves
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex(
phase => phase instanceof MovePhase && phase.pokemon === pokemon
);
const firstMovePhaseIndex = pokemon.scene.phaseQueue.findIndex(
phase => phase instanceof MovePhase
);
// Only shift MovePhase timing if it's not already next up
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase);
}
this.activated = true;
}
this.activated = true;
return true;
}
return super.lapse(pokemon, lapseType);
}
}
@ -641,7 +660,7 @@ export class ConfusedTag extends BattlerTag {
if (pokemon.randSeedInt(3) === 0) {
const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
const damage = toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
pokemon.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage);
pokemon.battleData.hitCount++;
@ -812,13 +831,13 @@ export class SeedTag extends BattlerTag {
if (ret) {
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (source) {
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED));
const damage = pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8));
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(),
!reverseDrain ? damage : damage * -1,
@ -838,7 +857,7 @@ export class SeedTag extends BattlerTag {
export class NightmareTag extends BattlerTag {
constructor() {
super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.AFTER_MOVE, 1, Moves.NIGHTMARE);
super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, Moves.NIGHTMARE);
}
onAdd(pokemon: Pokemon): void {
@ -860,11 +879,11 @@ export class NightmareTag extends BattlerTag {
pokemon.scene.queueMessage(i18next.t("battlerTags:nightmareLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 4));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4));
}
}
@ -929,10 +948,6 @@ export class EncoreTag extends BattlerTag {
return false;
}
if (allMoves[repeatableMove.move].hasAttr(ChargeAttr) && repeatableMove.result === MoveResult.OTHER) {
return false;
}
this.moveId = repeatableMove.move;
return true;
@ -1004,7 +1019,7 @@ export class IngrainTag extends TrappedTag {
new PokemonHealPhase(
pokemon.scene,
pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16),
toDmgValue(pokemon.getMaxHp() / 16),
i18next.t("battlerTags:ingrainLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }),
true
)
@ -1067,7 +1082,7 @@ export class AquaRingTag extends BattlerTag {
new PokemonHealPhase(
pokemon.scene,
pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16),
toDmgValue(pokemon.getMaxHp() / 16),
i18next.t("battlerTags:aquaRingLapse", {
moveName: this.getMoveName(),
pokemonName: getPokemonNameWithAffix(pokemon)
@ -1161,11 +1176,11 @@ export abstract class DamagingTrapTag extends TrappedTag {
);
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, this.commonAnim));
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
}
}
@ -1356,7 +1371,7 @@ export class ContactDamageProtectedTag extends ProtectedTag {
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon();
if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
}
}
}
@ -1709,7 +1724,7 @@ export class SemiInvulnerableTag extends BattlerTag {
onRemove(pokemon: Pokemon): void {
// Wait 2 frames before setting visible for battle animations that don't immediately show the sprite invisible
pokemon.scene.tweens.addCounter({
duration: Utils.getFrameMs(2),
duration: getFrameMs(2),
onComplete: () => pokemon.setVisible(true)
});
}
@ -1860,12 +1875,12 @@ export class SaltCuredTag extends BattlerTag {
if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE));
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
const pokemonSteelOrWater = pokemon.isOfType(Type.STEEL) || pokemon.isOfType(Type.WATER);
pokemon.damageAndUpdate(Utils.toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8));
pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8));
pokemon.scene.queueMessage(
i18next.t("battlerTags:saltCuredLapse", {
@ -1907,11 +1922,11 @@ export class CursedTag extends BattlerTag {
if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE));
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) {
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 4));
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4));
pokemon.scene.queueMessage(i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
}
@ -2173,7 +2188,7 @@ export class GulpMissileTag extends BattlerTag {
return true;
}
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
if (!cancelled.value) {
@ -2289,7 +2304,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag {
* @returns `true` if the move cannot be used because the target is an ally
*/
override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) {
const moveCategory = new Utils.IntegerHolder(allMoves[move].category);
const moveCategory = new NumberHolder(allMoves[move].category);
applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory);
if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) {
return true;
@ -2506,7 +2521,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
const ret = super.lapse(pokemon, lapseType);
if (lapseType === BattlerTagLapseType.CUSTOM) {
const cancelled = new Utils.BooleanHolder(false);
const cancelled = new BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) {
if (pokemon.mysteryEncounterBattleEffects) {
@ -2571,7 +2586,7 @@ export class TormentTag extends MoveRestrictionBattlerTag {
// This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY
// Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts
const moveObj = allMoves[lastMove.move];
const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr);
const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY);
const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS);
if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) {
return true;
@ -2955,3 +2970,22 @@ export function loadBattlerTag(source: BattlerTag | any): BattlerTag {
tag.loadTag(source);
return tag;
}
/**
* Helper function to verify that the current phase is a MoveEffectPhase and provide quick access to commonly used fields
*
* @param pokemon {@linkcode Pokemon} The Pokémon used to access the current phase
* @returns null if current phase is not MoveEffectPhase, otherwise Object containing the {@linkcode MoveEffectPhase}, and its
* corresponding {@linkcode Move} and user {@linkcode Pokemon}
*/
function getMoveEffectPhaseData(pokemon: Pokemon): {phase: MoveEffectPhase, attacker: Pokemon, move: Move} | null {
const phase = pokemon.scene.getCurrentPhase();
if (phase instanceof MoveEffectPhase) {
return {
phase : phase,
attacker : phase.getPokemon(),
move : phase.move.getMove()
};
}
return null;
}

View File

@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
import * as Utils from "../utils";
import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain";
@ -288,10 +288,9 @@ export default class Move implements Localizable {
}
/**
* Getter function that returns if the move targets itself or an ally
* Getter function that returns if the move targets the user or its ally
* @returns boolean
*/
isAllyTarget(): boolean {
switch (this.moveTarget) {
case MoveTarget.USER:
@ -305,6 +304,10 @@ export default class Move implements Localizable {
return false;
}
isChargingMove(): this is ChargingMove {
return false;
}
/**
* Checks if the move is immune to certain types.
* Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster.
@ -345,7 +348,11 @@ export default class Move implements Localizable {
return false;
}
return !user.hasAbility(Abilities.INFILTRATOR)
const bypassed = new Utils.BooleanHolder(false);
// TODO: Allow this to be simulated
applyAbAttrs(InfiltratorAbAttr, user, null, false, bypassed);
return !bypassed.value
&& !this.hasFlag(MoveFlags.SOUND_BASED)
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
}
@ -878,6 +885,85 @@ export class SelfStatusMove extends Move {
}
}
type SubMove = new (...args: any[]) => Move;
function ChargeMove<TBase extends SubMove>(Base: TBase) {
return class extends Base {
/** The animation to play during the move's charging phase */
public readonly chargeAnim: ChargeAnim = ChargeAnim[`${Moves[this.id]}_CHARGING`];
/** The message to show during the move's charging phase */
private _chargeText: string;
/** Move attributes that apply during the move's charging phase */
public chargeAttrs: MoveAttr[] = [];
override isChargingMove(): this is ChargingMove {
return true;
}
/**
* Sets the text to be displayed during this move's charging phase.
* References to the user Pokemon should be written as "{USER}", and
* references to the target Pokemon should be written as "{TARGET}".
* @param chargeText the text to set
* @returns this {@linkcode Move} (for chaining API purposes)
*/
chargeText(chargeText: string): this {
this._chargeText = chargeText;
return this;
}
/**
* Queues the charge text to display to the player
* @param user the {@linkcode Pokemon} using this move
* @param target the {@linkcode Pokemon} targeted by this move (optional)
*/
showChargeText(user: Pokemon, target?: Pokemon): void {
user.scene.queueMessage(this._chargeText
.replace("{USER}", getPokemonNameWithAffix(user))
.replace("{TARGET}", getPokemonNameWithAffix(target))
);
}
/**
* Gets all charge attributes of the given attribute type.
* @param attrType any attribute that extends {@linkcode MoveAttr}
* @returns Array of attributes that match `attrType`, or an empty array if
* no matches are found.
*/
getChargeAttrs<T extends MoveAttr>(attrType: Constructor<T>): T[] {
return this.chargeAttrs.filter((attr): attr is T => attr instanceof attrType);
}
/**
* Checks if this move has an attribute of the given type.
* @param attrType any attribute that extends {@linkcode MoveAttr}
* @returns `true` if a matching attribute is found; `false` otherwise
*/
hasChargeAttr<T extends MoveAttr>(attrType: Constructor<T>): boolean {
return this.chargeAttrs.some((attr) => attr instanceof attrType);
}
/**
* Adds an attribute to this move to be applied during the move's charging phase
* @param ChargeAttrType the type of {@linkcode MoveAttr} being added
* @param args the parameters to construct the given {@linkcode MoveAttr} with
* @returns this {@linkcode Move} (for chaining API purposes)
*/
chargeAttr<T extends Constructor<MoveAttr>>(ChargeAttrType: T, ...args: ConstructorParameters<T>): this {
const chargeAttr = new ChargeAttrType(...args);
this.chargeAttrs.push(chargeAttr);
return this;
}
};
}
export class ChargingAttackMove extends ChargeMove(AttackMove) {}
export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove) {}
export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove;
/**
* Base class defining all {@linkcode Move} Attributes
* @abstract
@ -2035,15 +2121,15 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
export class StatusEffectAttr extends MoveEffectAttr {
public effect: StatusEffect;
public cureTurn: integer | null;
public overrideStatus: boolean;
public turnsRemaining?: number;
public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) {
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, MoveEffectTrigger.HIT);
this.effect = effect;
this.cureTurn = cureTurn!; // TODO: is this bang correct?
this.overrideStatus = !!overrideStatus;
this.turnsRemaining = turnsRemaining;
this.overrideStatus = overrideStatus;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -2063,14 +2149,14 @@ export class StatusEffectAttr extends MoveEffectAttr {
}
}
if (user !== target && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) {
if (user !== target && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
return false;
}
if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) {
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining)) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true;
}
@ -2087,8 +2173,8 @@ export class StatusEffectAttr extends MoveEffectAttr {
export class MultiStatusEffectAttr extends StatusEffectAttr {
public effects: StatusEffect[];
constructor(effects: StatusEffect[], selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) {
super(effects[0], selfTarget, cureTurn, overrideStatus);
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
this.effects = effects;
}
@ -2559,6 +2645,63 @@ export class OneHitKOAttr extends MoveAttr {
}
}
/**
* Attribute that allows charge moves to resolve in 1 turn under a given condition.
* Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
* @extends MoveAttr
*/
export class InstantChargeAttr extends MoveAttr {
/** The condition in which the move with this attribute instantly charges */
protected readonly condition: UserMoveConditionFunc;
constructor(condition: UserMoveConditionFunc) {
super(true);
this.condition = condition;
}
/**
* Flags the move with this attribute as instantly charged if this attribute's condition is met.
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move the {@linkcode Move} associated with this attribute
* @param args
* - `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} for the "instant charge" flag
* @returns `true` if the instant charge condition is met; `false` otherwise.
*/
override apply(user: Pokemon, target: Pokemon | null, move: Move, args: any[]): boolean {
const instantCharge = args[0];
if (!(instantCharge instanceof Utils.BooleanHolder)) {
return false;
}
if (this.condition(user, move)) {
instantCharge.value = true;
return true;
}
return false;
}
}
/**
* Attribute that allows charge moves to resolve in 1 turn while specific {@linkcode WeatherType | Weather}
* is active. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
* @extends InstantChargeAttr
*/
export class WeatherInstantChargeAttr extends InstantChargeAttr {
constructor(weatherTypes: WeatherType[]) {
super((user, move) => {
const currentWeather = user.scene.arena.weather;
if (Utils.isNullOrUndefined(currentWeather?.weatherType)) {
return false;
} else {
return !currentWeather?.isEffectSuppressed(user.scene)
&& weatherTypes.includes(currentWeather?.weatherType);
}
});
}
}
export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
//const overridden = args[0] as Utils.BooleanHolder;
@ -2567,112 +2710,6 @@ export class OverrideMoveEffectAttr extends MoveAttr {
}
}
export class ChargeAttr extends OverrideMoveEffectAttr {
public chargeAnim: ChargeAnim;
private chargeText: string;
private tagType: BattlerTagType | null;
private chargeEffect: boolean;
public followUpPriority: integer | null;
constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false) {
super();
this.chargeAnim = chargeAnim;
this.chargeText = chargeText;
this.tagType = tagType!; // TODO: is this bang correct?
this.chargeEffect = chargeEffect;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
const lastMove = user.getLastXMoves().find(() => true);
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) {
(args[0] as Utils.BooleanHolder).value = true;
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
if (this.tagType) {
user.addTag(this.tagType, 1, move.id, user.id);
}
if (this.chargeEffect) {
applyMoveAttrs(MoveEffectAttr, user, target, move);
}
const isVirtual = args[1] as boolean;
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], virtual: isVirtual });
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
resolve(true);
});
} else {
user.lapseTag(BattlerTagType.CHARGING);
resolve(false);
}
});
}
usedChargeEffect(user: Pokemon, target: Pokemon | null, move: Move): boolean {
if (!this.chargeEffect) {
return false;
}
// Account for move history being populated when this function is called
const lastMoves = user.getLastXMoves(2);
return lastMoves.length === 2 && lastMoves[1].move === move.id && lastMoves[1].result === MoveResult.OTHER;
}
}
export class SunlightChargeAttr extends ChargeAttr {
constructor(chargeAnim: ChargeAnim, chargeText: string) {
super(chargeAnim, chargeText);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
const weatherType = user.scene.arena.weather?.weatherType;
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN)) {
resolve(false);
} else {
super.apply(user, target, move, args).then(result => resolve(result));
}
});
}
}
export class ElectroShotChargeAttr extends ChargeAttr {
private statIncreaseApplied: boolean;
constructor() {
super(ChargeAnim.ELECTRO_SHOT_CHARGING, i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }), null, true);
// Add a flag because ChargeAttr skills use themselves twice instead of once over one-to-two turns
this.statIncreaseApplied = false;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
const weatherType = user.scene.arena.weather?.weatherType;
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) {
// Apply the SPATK increase every call when used in the rain
const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
statChangeAttr.apply(user, target, move, args);
// After the SPATK is raised, execute the move resolution e.g. deal damage
resolve(false);
} else {
if (!this.statIncreaseApplied) {
// Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation
const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
statChangeAttr.apply(user, target, move, args);
// Set the flag to true so that on the following turn it doesn't raise SPATK a second time
this.statIncreaseApplied = true;
}
super.apply(user, target, move, args).then(result => {
if (!result) {
// On the second turn, reset the statIncreaseApplied flag without applying the SPATK increase
this.statIncreaseApplied = false;
}
resolve(result);
});
}
});
}
}
export class DelayedAttackAttr extends OverrideMoveEffectAttr {
public tagType: ArenaTagType;
public chargeAnim: ChargeAnim;
@ -4864,6 +4901,37 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
return true;
};
/**
* Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during
* the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
* @extends MoveEffectAttr
*/
export class SemiInvulnerableAttr extends MoveEffectAttr {
/** The type of {@linkcode SemiInvulnerableTag} to grant to the user */
public tagType: BattlerTagType;
constructor(tagType: BattlerTagType) {
super(true);
this.tagType = tagType;
}
/**
* Grants a {@linkcode SemiInvulnerableTag} to the associated move's user.
* @param user the {@linkcode Pokemon} using the move
* @param target n/a
* @param move the {@linkcode Move} being used
* @param args n/a
* @returns `true` if semi-invulnerability was successfully granted; `false` otherwise.
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
return user.addTag(this.tagType, 1, move.id, user.id);
}
}
export class AddBattlerTagAttr extends MoveEffectAttr {
public tagType: BattlerTagType;
public turnCountMin: integer;
@ -5151,7 +5219,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) {
if (!this.selfTarget && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
@ -6566,7 +6634,7 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
return false;
}
if (allMoves[copiableMove.move].hasAttr(ChargeAttr) && copiableMove.result === MoveResult.OTHER) {
if (allMoves[copiableMove.move].isChargingMove() && copiableMove.result === MoveResult.OTHER) {
return false;
}
@ -6858,42 +6926,52 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
}
export class TransformAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
if (!super.apply(user, target, move, args)) {
return resolve(false);
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
if (!super.apply(user, target, move, args)) {
return false;
}
const promises: Promise<void>[] = [];
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
user.summonData.fusionGender = target.getFusionGender();
// Power Trick's effect will not preserved after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false);
}
// Copy all stat stages
for (const s of BATTLE_STATS) {
user.setStatStage(s, target.getStatStage(s));
}
user.summonData.moveset = target.getMoveset().map((m) => {
if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
} else {
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(Moves.NONE);
}
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
user.summonData.fusionGender = target.getFusionGender();
// Power Trick's effect will not preserved after using Transform
user.removeTag(BattlerTagType.POWER_TRICK);
// Copy all stats (except HP)
for (const s of EFFECTIVE_STATS) {
user.setStat(s, target.getStat(s, false), false);
}
// Copy all stat stages
for (const s of BATTLE_STATS) {
user.setStatStage(s, target.getStatStage(s));
}
user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct?
user.summonData.types = target.getTypes();
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
resolve(true);
});
});
user.summonData.types = target.getTypes();
promises.push(user.updateInfo());
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
promises.push(user.loadAssets(false).then(() => {
user.playAnim();
user.updateInfo();
}));
await Promise.all(promises);
return true;
}
}
@ -7265,6 +7343,20 @@ function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null
});
}
function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise<void> {
return new Promise(resolve => {
const chargeAttrPromises: Promise<boolean>[] = [];
const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a));
for (const attr of chargeMoveAttrs) {
const result = attr.apply(user, target, move, args);
if (result instanceof Promise) {
chargeAttrPromises.push(result);
}
}
Promise.allSettled(chargeAttrPromises).then(() => resolve());
});
}
export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> {
return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
}
@ -7273,6 +7365,10 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon
return applyMoveAttrsInternal(attrFilter, user, target, move, args);
}
export function applyMoveChargeAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise<void> {
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
}
export class MoveCondition {
protected func: MoveConditionFunc;
@ -7520,8 +7616,8 @@ export function initMoves() {
new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr),
new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" }))
new ChargingAttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" }))
.attr(HighCritAttr)
.windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
@ -7540,8 +7636,9 @@ export function initMoves() {
.hidesTarget()
.windMove()
.partial(), // Should force random switches
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }), BattlerTagType.FLYING)
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition),
new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
.attr(TrapAttr, BattlerTagType.BIND),
@ -7689,8 +7786,9 @@ export function initMoves() {
.makesContact(false)
.slicingMove()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" }))
new ChargingAttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
.attr(AntiSunlightPowerDecreaseAttr),
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON)
@ -7738,8 +7836,9 @@ export function initMoves() {
.attr(OneHitKOAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
.makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }), BattlerTagType.UNDERGROUND),
new ChargingAttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND),
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.TOXIC)
.attr(ToxicAccuracyAttr),
@ -7830,9 +7929,9 @@ export function initMoves() {
.attr(TrapAttr, BattlerTagType.CLAMP),
new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }), null, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new ChargingAttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }))
.chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1)
.attr(MultiHitAttr)
.makesContact(false),
@ -7868,13 +7967,14 @@ export function initMoves() {
.triageMove(),
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP),
new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
.attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.attr(HighCritAttr)
.attr(FlinchAttr)
.makesContact(false),
new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(TransformAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.ignoresProtect(),
new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
@ -8298,7 +8398,7 @@ export function initMoves() {
.attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
.condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)),
.condition((user, target, move) => !target.status && !target.isSafeguarded(user)),
new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false),
@ -8325,9 +8425,10 @@ export function initMoves() {
new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3)
.makesContact(false)
.attr(SecretPowerAttr),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true)
.attr(GulpMissileTagAttr),
new ChargingAttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
.chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER)
.chargeAttr(GulpMissileTagAttr),
new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3)
.attr(MultiHitAttr),
new SelfStatusMove(Moves.CAMOUFLAGE, Type.NORMAL, -1, 20, -1, 0, 3)
@ -8459,8 +8560,9 @@ export function initMoves() {
.attr(RechargeAttr),
new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true),
new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
.attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }), BattlerTagType.FLYING)
new ChargingAttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
.chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.condition(failOnGravityCondition),
new AttackMove(Moves.MUD_SHOT, Type.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3)
@ -8812,8 +8914,9 @@ export function initMoves() {
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.windMove(),
new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
.attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN)
new ChargingAttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
.chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect(),
new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true),
@ -8935,11 +9038,12 @@ export function initMoves() {
.attr(
MovePowerMultiplierAttr,
(user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1),
new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.partial() // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }), BattlerTagType.FLYING) // TODO: Add 2nd turn message
new ChargingAttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)),
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
@ -9089,12 +9193,12 @@ export function initMoves() {
new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.danceMove(),
new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5)
.attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" }))
new ChargingAttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5)
.chargeText(i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" }))
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.makesContact(false),
new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5)
.attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" }))
new ChargingAttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5)
.chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" }))
.attr(StatusEffectAttr, StatusEffect.BURN),
new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
@ -9135,8 +9239,9 @@ export function initMoves() {
.target(MoveTarget.ENEMY_SIDE),
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN)
new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GHOST)
@ -9245,8 +9350,8 @@ export function initMoves() {
.ignoresSubstitute()
.powderMove()
.unimplemented(),
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)))
@ -9417,8 +9522,9 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
.triageMove(),
new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
.attr(AntiSunlightPowerDecreaseAttr)
.slicingMove(),
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
@ -9808,9 +9914,9 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
.attr(MultiHitAttr)
.makesContact(false),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }), null, true)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true),
new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
.chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }))
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true),
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr)
.attr(StatusEffectAttr, StatusEffect.POISON)
@ -10261,8 +10367,10 @@ export function initMoves() {
.attr(IvyCudgelTypeAttr)
.attr(HighCritAttr)
.makesContact(false),
new AttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
.attr(ElectroShotChargeAttr),
new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
.chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }))
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]),
new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(TeraMoveCategoryAttr)
.attr(TeraStarstormTypeAttr)

View File

@ -1,4 +1,4 @@
import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { modifierTypes } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
@ -14,6 +14,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import i18next from "i18next";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/anOfferYouCantRefuse";
@ -98,6 +99,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
}
}
const shinyCharm = generateModifierType(scene, modifierTypes.SHINY_CHARM);
encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name"));
encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName());
return true;
@ -123,7 +126,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
return true;
})
.withOptionPhase(async (scene: BattleScene) => {
// Give the player a Shiny charm
// Give the player a Shiny Charm
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM));
leaveEncounterWithoutBattle(scene, true);
})
@ -132,9 +135,11 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
new MoveRequirement(EXTORTION_MOVES, true),
new AbilityRequirement(EXTORTION_ABILITIES, true))
.withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
new MoveRequirement(EXTORTION_MOVES, true),
new AbilityRequirement(EXTORTION_ABILITIES, true)
)
)
.withDialogue({
buttonLabel: `${namespace}:option.2.label`,

View File

@ -193,12 +193,14 @@ const WAVE_LEVEL_BREAKPOINTS = [ 30, 50, 70, 100, 120, 140, 160 ];
export const BugTypeSuperfanEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN)
.withEncounterTier(MysteryEncounterTier.GREAT)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1),
new TypeRequirement(Type.BUG, false, 1)
))
.withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1),
new TypeRequirement(Type.BUG, false, 1)
)
)
.withMaxAllowedEncounters(1)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([]) // These are set in onInit()
@ -405,11 +407,13 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
.build())
.withOption(MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
// Meets one or both of the below reqs
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1)
))
.withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
// Meets one or both of the below reqs
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1)
)
)
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -11,7 +11,7 @@ import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Abilities } from "#enums/abilities";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Type } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -425,17 +425,8 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Do ability swap
const encounter = scene.currentBattle.mysteryEncounter!;
if (pokemon.isFusion()) {
if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionCustomPokemonData = new CustomPokemonData();
}
pokemon.fusionCustomPokemonData.ability = encounter.misc.ability;
} else {
if (!pokemon.customPokemonData) {
pokemon.customPokemonData = new CustomPokemonData();
}
pokemon.customPokemonData.ability = encounter.misc.ability;
}
applyAbilityOverrideToPokemon(pokemon, encounter.misc.ability);
encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));
};

View File

@ -14,6 +14,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges";
/** i18n namespace for encounter */
const namespace = "mysteryEncounters/darkDeal";
@ -141,6 +142,7 @@ export const DarkDealEncounter: MysteryEncounter =
// Removes random pokemon (including fainted) from party and adds name to dialogue data tokens
// Will never return last battle able mon and instead pick fainted/unable to battle
const removedPokemon = getRandomPlayerPokemon(scene, true, false, true);
// Get all the pokemon's held items
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removedPokemon);
@ -160,7 +162,13 @@ export const DarkDealEncounter: MysteryEncounter =
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL));
// Start encounter with random legendary (7-10 starter strength) that has level additive
const bossTypes: Type[] = encounter.misc.removedTypes;
// If this is a mono-type challenge, always ensure the required type is filtered for
let bossTypes: Type[] = encounter.misc.removedTypes;
const singleTypeChallenges = scene.gameMode.challenges.filter(c => c.value && c.id === Challenges.SINGLE_TYPE);
if (scene.gameMode.isChallenge && singleTypeChallenges.length > 0) {
bossTypes = singleTypeChallenges.map(c => (c.value - 1) as Type);
}
const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers;
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
const roll = randSeedInt(100);
@ -172,7 +180,8 @@ export const DarkDealEncounter: MysteryEncounter =
isBoss: true,
modifierConfigs: bossModifiers.map(m => {
return {
modifier: m
modifier: m,
stackCount: m.getStackCount(),
};
})
};

View File

@ -45,10 +45,13 @@ export const DelibirdyEncounter: MysteryEncounter =
.withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)
))
.withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
// Must also have either option 2 or 3 available to spawn
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)
)
)
.withIntroSpriteConfigs([
{
spriteKey: "",
@ -196,7 +199,7 @@ export const DelibirdyEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier;
// Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed
// Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed
if (modifier instanceof BerryModifier) {
// Check if the player has max stacks of that Candy Jar already
const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier;
@ -211,8 +214,8 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR));
}
} else {
// Check if the player has max stacks of that Healing Charm already
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
// Check if the player has max stacks of that Berry Pouch already
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead
@ -221,7 +224,7 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else {
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
}
}
@ -290,8 +293,8 @@ export const DelibirdyEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier;
// Check if the player has max stacks of Berry Pouch already
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
// Check if the player has max stacks of Healing Charm already
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead
@ -300,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else {
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
}
// Remove the modifier if its stacks go to 0

View File

@ -4,24 +4,30 @@ import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/mod
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Gender } from "#app/data/gender";
import { Type } from "#app/data/type";
import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon";
import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims";
import { WeatherType } from "#app/data/weather";
import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { StatusEffect } from "#app/data/status-effect";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { Ability } from "#app/data/ability";
import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieryFallout";
@ -62,16 +68,24 @@ export const FieryFalloutEncounter: MysteryEncounter =
{
species: volcaronaSpecies,
isBoss: false,
gender: Gender.MALE
gender: Gender.MALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1));
}
},
{
species: volcaronaSpecies,
isBoss: false,
gender: Gender.FEMALE
gender: Gender.FEMALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1));
}
}
],
doubleBattle: true,
disableSwitch: true
disableSwitch: true,
};
encounter.enemyPartyConfigs = [ config ];
@ -139,7 +153,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
async (scene: BattleScene) => {
// Pick battle
const encounter = scene.currentBattle.mysteryEncounter!;
setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene));
setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonAttackTypeBoostItem(scene));
encounter.startOfBattleEffects.push(
{
@ -153,18 +167,6 @@ export const FieryFalloutEncounter: MysteryEncounter =
targets: [ BattlerIndex.PLAYER_2 ],
move: new PokemonMove(Moves.FIRE_SPIN),
ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [ BattlerIndex.ENEMY ],
move: new PokemonMove(Moves.QUIVER_DANCE),
ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [ BattlerIndex.ENEMY_2 ],
move: new PokemonMove(Moves.QUIVER_DANCE),
ignorePp: true
});
await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
}
@ -180,7 +182,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
],
},
async (scene: BattleScene) => {
// Damage non-fire types and burn 1 random non-fire type member
// Damage non-fire types and burn 1 random non-fire type member + give it Heatproof
const encounter = scene.currentBattle.mysteryEncounter!;
const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE));
@ -198,7 +200,11 @@ export const FieryFalloutEncounter: MysteryEncounter =
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
// Burn applied
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
encounter.setDialogueToken("abilityName", new Ability(Abilities.HEATPROOF, 3).name);
queueEncounterMessage(scene, `${namespace}:option.2.target_burned`);
// Also permanently change the burned Pokemon's ability to Heatproof
applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF);
}
}
@ -209,8 +215,12 @@ export const FieryFalloutEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically
.withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically
.withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some(
new TypeRequirement(Type.FIRE, true, 1),
new AbilityRequirement(FIRE_RESISTANT_ABILITIES, true)
)
) // Will set option3PrimaryName dialogue token automatically
.withDialogue({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,
@ -233,26 +243,32 @@ export const FieryFalloutEncounter: MysteryEncounter =
{ fillRemaining: true },
undefined,
() => {
giveLeadPokemonCharcoal(scene);
giveLeadPokemonAttackTypeBoostItem(scene);
});
const primary = encounter.options[2].primaryPokemon!;
const secondary = encounter.options[2].secondaryPokemon![0];
setEncounterExp(scene, [ primary.id, secondary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2);
setEncounterExp(scene, [ primary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2);
leaveEncounterWithoutBattle(scene);
})
.build()
)
.build();
function giveLeadPokemonCharcoal(scene: BattleScene) {
// Give first party pokemon Charcoal for free at end of battle
function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) {
// Give first party pokemon attack type boost item for free at end of battle
const leadPokemon = scene.getParty()?.[0];
if (leadPokemon) {
const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType;
applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal);
scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
queueEncounterMessage(scene, `${namespace}:found_charcoal`);
// Generate type booster held item, default to Charcoal if item fails to generate
let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType;
if (!boosterModifierType) {
boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType;
}
applyModifierTypeToPlayerPokemon(scene, leadPokemon, boosterModifierType);
const encounter = scene.currentBattle.mysteryEncounter!;
encounter.setDialogueToken("itemName", boosterModifierType.name);
encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
queueEncounterMessage(scene, `${namespace}:found_item`);
}
}

View File

@ -56,7 +56,13 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
// Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config
// Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100
const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
let retries = 0;
let hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
while (retries < 5 && hardTrainerType === normalTrainerType) {
// Will try to use a different trainer from the normal trainer type
hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
retries++;
}
const hardTemplate = new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true),
new TrainerPartyTemplate(

View File

@ -21,7 +21,7 @@ import i18next from "i18next";
const namespace = "mysteryEncounters/shadyVitaminDealer";
const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5;
const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5;
const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 5;
/**
* Shady Vitamin Dealer encounter.

View File

@ -222,7 +222,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon1;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
setEncounterRewards(scene,
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
eggOptions,
() => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
@ -271,7 +274,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon2;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
setEncounterRewards(scene,
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
eggOptions,
() => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
@ -320,7 +326,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon3;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
setEncounterRewards(scene,
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
eggOptions,
() => doPostEncounterCleanup(scene));
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
@ -454,12 +463,16 @@ function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number]
}
// Maximum of 30 points
const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
let totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
// 1 Rare egg for every 6 points
const numRares = Math.floor(totalPoints / 6);
// First 5 points go to Common eggs
let numCommons = Math.min(totalPoints, 5);
totalPoints -= numCommons;
// Then, 1 Rare egg for every 4 points
const numRares = Math.floor(totalPoints / 4);
// 1 Common egg for every point leftover
const numCommons = totalPoints % 6;
numCommons += totalPoints % 4;
return [ numCommons, numRares ];
}

View File

@ -37,6 +37,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
.withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party
.withFleeAllowed(false)
.withHideWildIntroMessage(true)
.withPreventGameStatsUpdates(true) // Do not count the Pokemon as seen or defeated since it is ours
.withIntroSpriteConfigs([
{
spriteKey: "training_session_gear",

View File

@ -71,7 +71,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ]
};
const config: EnemyPartyConfig = {
levelAdditiveModifier: 1,
levelAdditiveModifier: 0.5,
pokemonConfigs: [ pokemonConfig ],
disableSwitch: true
};

View File

@ -4,23 +4,30 @@ import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils";
import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils";
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifierTypes } from "#app/modifier/modifier-type";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import i18next from "#app/plugins/i18n";
import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { PlayerGender } from "#enums/player-gender";
import { TrainerType } from "#enums/trainer-type";
import PokemonData from "#app/system/pokemon-data";
import { Nature } from "#enums/nature";
import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import { trainerConfigs, TrainerPartyTemplate } from "#app/data/trainer-config";
import { PartyMemberStrength } from "#enums/party-member-strength";
/** i18n namespace for encounter */
const namespace = "mysteryEncounters/weirdDream";
@ -80,10 +87,11 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [
const SUPER_LEGENDARY_BST_THRESHOLD = 600;
const NON_LEGENDARY_BST_THRESHOLD = 570;
const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450;
const OLD_GATEAU_STATS_UP = 20;
/** 0-100 */
const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5;
const PERCENT_LEVEL_LOSS_ON_REFUSE = 10;
/**
* Value ranges of the resulting species BST transformations after adding values to original species
@ -105,7 +113,8 @@ export const WeirdDreamEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM)
.withEncounterTier(MysteryEncounterTier.ROGUE)
.withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
// TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now.
.withSceneWaveRangeRequirement(30, 140)
.withIntroSpriteConfigs([
{
spriteKey: "weird_dream_woman",
@ -131,6 +140,15 @@ export const WeirdDreamEncounter: MysteryEncounter =
.withQuery(`${namespace}:query`)
.withOnInit((scene: BattleScene) => {
scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3");
// Calculate all the newly transformed Pokemon and begin asset load
const teamTransformations = getTeamTransformations(scene);
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets());
scene.currentBattle.mysteryEncounter!.misc = {
teamTransformations,
loadAssets
};
return true;
})
.withOnVisualsStart((scene: BattleScene) => {
@ -156,13 +174,10 @@ export const WeirdDreamEncounter: MysteryEncounter =
doShowDreamBackground(scene);
});
// Calculate all the newly transformed Pokemon and begin asset load
const teamTransformations = getTeamTransformations(scene);
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets());
scene.currentBattle.mysteryEncounter!.misc = {
teamTransformations,
loadAssets
};
for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) {
scene.removePokemonFromPlayerParty(transformation.previousPokemon, false);
scene.getParty().push(transformation.newPokemon);
}
})
.withOptionPhase(async (scene: BattleScene) => {
// Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue
@ -193,7 +208,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
await showEncounterText(scene, `${namespace}:option.1.dream_complete`);
await doNewTeamPostProcess(scene, transformations);
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT ]});
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT, modifierTypes.MINT ], fillRemaining: false });
leaveEncounterWithoutBattle(scene, true);
})
.build()
@ -209,7 +224,88 @@ export const WeirdDreamEncounter: MysteryEncounter =
],
},
async (scene: BattleScene) => {
// Reduce party levels by 20%
// Battle your "future" team for some item rewards
const transformations: PokemonTransformation[] = scene.currentBattle.mysteryEncounter!.misc.teamTransformations;
// Uses the pokemon that player's party would have transformed into
const enemyPokemonConfigs: EnemyPokemonConfig[] = [];
for (const transformation of transformations) {
const newPokemon = transformation.newPokemon;
const previousPokemon = transformation.previousPokemon;
await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, newPokemon.species.getRootSpeciesId(), true);
const dataSource = new PokemonData(newPokemon);
dataSource.player = false;
// Copy held items to new pokemon
const newPokemonHeldItemConfigs: HeldModifierConfig[] = [];
for (const item of transformation.heldItems) {
newPokemonHeldItemConfigs.push({
modifier: item.clone() as PokemonHeldItemModifier,
stackCount: item.getStackCount(),
isTransferable: false
});
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon);
newPokemonHeldItemConfigs.push({
modifier: generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ OLD_GATEAU_STATS_UP, stats ]) as PokemonHeldItemModifierType,
stackCount: 1,
isTransferable: false
});
}
const enemyConfig: EnemyPokemonConfig = {
species: transformation.newSpecies,
isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD,
level: previousPokemon.level,
dataSource: dataSource,
modifierConfigs: newPokemonHeldItemConfigs
};
enemyPokemonConfigs.push(enemyConfig);
}
const genderIndex = scene.gameData.gender ?? PlayerGender.UNSET;
const trainerConfig = trainerConfigs[genderIndex === PlayerGender.FEMALE ? TrainerType.FUTURE_SELF_F : TrainerType.FUTURE_SELF_M].clone();
trainerConfig.setPartyTemplates(new TrainerPartyTemplate(transformations.length, PartyMemberStrength.STRONG));
const enemyPartyConfig: EnemyPartyConfig = {
trainerConfig: trainerConfig,
pokemonConfigs: enemyPokemonConfigs,
female: genderIndex === PlayerGender.FEMALE
};
const onBeforeRewards = () => {
// Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently)
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
enablePassiveMon.passive = true;
enablePassiveMon.updateInfo(true);
}
};
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: false }, undefined, onBeforeRewards);
await showEncounterText(scene, `${namespace}:option.2.selected_2`, null, undefined, true);
await initBattleWithEnemyConfig(scene, enemyPartyConfig);
}
)
.withSimpleOption(
{
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,
selected: [
{
text: `${namespace}:option.3.selected`,
},
],
},
async (scene: BattleScene) => {
// Leave, reduce party levels by 10%
for (const pokemon of scene.getParty()) {
pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1);
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
@ -235,7 +331,7 @@ interface PokemonTransformation {
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
const party = scene.getParty();
// Removes all pokemon from the party
const alreadyUsedSpecies: PokemonSpecies[] = [];
const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species);
const pokemonTransformations: PokemonTransformation[] = party.map(p => {
return {
previousPokemon: p
@ -250,11 +346,11 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
// First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference
// Then, roll the remainder of the party members at a +40 to +50 BST difference
const numPokemon = party.length;
const removedPokemon = randSeedShuffle(party.slice(0));
for (let i = 0; i < numPokemon; i++) {
const removed = party[randSeedInt(party.length)];
const removed = removedPokemon[i];
const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removed, false);
const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0);
let newBstRange: [number, number];
@ -276,14 +372,13 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
pokemonTransformations[index].newSpecies = newSpecies;
console.log("New species: " + JSON.stringify(newSpecies));
alreadyUsedSpecies.push(newSpecies);
}
for (const transformation of pokemonTransformations) {
const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount());
const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
transformation.newPokemon = newPlayerPokemon;
scene.getParty().push(newPlayerPokemon);
transformation.newPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
}
return pokemonTransformations;
@ -296,109 +391,20 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId();
// Roll HA a second time
if (newPokemon.species.abilityHidden) {
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
if (newPokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new IntegerHolder(256);
scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
if (hasHiddenAbility) {
newPokemon.abilityIndex = hiddenIndex;
}
}
if (await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, speciesRootForm)) {
atLeastOneNewStarter = true;
}
// Roll IVs a second time
newPokemon.ivs = newPokemon.ivs.map(iv => {
const newValue = randSeedInt(31);
return newValue > iv ? newValue : iv;
});
// For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) {
if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) {
scene.validateAchv(achvs.HIDDEN_ABILITY);
}
if (newPokemon.species.subLegendary) {
scene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
}
if (newPokemon.species.legendary) {
scene.validateAchv(achvs.CATCH_LEGENDARY);
}
if (newPokemon.species.mythical) {
scene.validateAchv(achvs.CATCH_MYTHICAL);
}
scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs);
const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false);
if (newStarterUnlocked) {
atLeastOneNewStarter = true;
await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() }));
}
}
// If the previous pokemon had pokerus, transfer to new pokemon
newPokemon.pokerus = previousPokemon.pokerus;
// Transfer previous Pokemon's luck value
newPokemon.luck = previousPokemon.getLuck();
// If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock)
newPokemon.ivs = newPokemon.ivs.map((iv, index) => {
return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv;
});
// For pokemon that the player owns (including ones just caught), gain a candy
if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) {
scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1);
}
// Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species
newPokemon.generateAndPopulateMoveset();
// Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move
const newPokemonGeneratedMoveset = newPokemon.moveset;
newPokemon.moveset = previousPokemon.moveset;
const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm);
// Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset)
addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex);
// Randomize the second type of the pokemon
// If the pokemon does not normally have a second type, it will gain 1
const newTypes = [ newPokemon.getTypes()[0] ];
let newType = randSeedInt(18) as Type;
while (newType === newTypes[0]) {
newType = randSeedInt(18) as Type;
}
newTypes.push(newType);
if (!newPokemon.customPokemonData) {
newPokemon.customPokemonData = new CustomPokemonData();
}
newPokemon.customPokemonData.types = newTypes;
// Copy old items to new pokemon
for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id;
await scene.addModifier(item, false, false, false, true);
}
// Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) {
const stats: Stat[] = [ Stat.HP ];
const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon);
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU()
.generateType(scene.getParty(), [ 20, stats ])
.generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon);
if (modifier) {
@ -406,9 +412,6 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
}
}
// Enable passive if previous had it
newPokemon.passive = previousPokemon.passive;
newPokemon.calculateStats();
await newPokemon.updateInfo();
}
@ -427,6 +430,138 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
}
}
/**
* Applies special changes to the newly transformed pokemon, such as passing previous moves, gaining egg moves, etc.
* Returns whether the transformed pokemon unlocks a new starter for the player.
* @param scene
* @param previousPokemon
* @param newPokemon
* @param speciesRootForm
* @param forBattle Default `false`. If false, will perform achievements and dex unlocks for the player.
*/
async function postProcessTransformedPokemon(scene: BattleScene, previousPokemon: PlayerPokemon, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise<boolean> {
let isNewStarter = false;
// Roll HA a second time
if (newPokemon.species.abilityHidden) {
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
if (newPokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new IntegerHolder(256);
scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
if (hasHiddenAbility) {
newPokemon.abilityIndex = hiddenIndex;
}
}
}
// Roll IVs a second time
newPokemon.ivs = newPokemon.ivs.map(iv => {
const newValue = randSeedInt(31);
return newValue > iv ? newValue : iv;
});
// Roll a neutral nature
newPokemon.nature = [ Nature.HARDY, Nature.DOCILE, Nature.BASHFUL, Nature.QUIRKY, Nature.SERIOUS ][randSeedInt(5)];
// For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it
if (!forBattle && (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny())) {
if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) {
scene.validateAchv(achvs.HIDDEN_ABILITY);
}
if (newPokemon.species.subLegendary) {
scene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
}
if (newPokemon.species.legendary) {
scene.validateAchv(achvs.CATCH_LEGENDARY);
}
if (newPokemon.species.mythical) {
scene.validateAchv(achvs.CATCH_MYTHICAL);
}
scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs);
const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false);
if (newStarterUnlocked) {
isNewStarter = true;
await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() }));
}
}
// If the previous pokemon had pokerus, transfer to new pokemon
newPokemon.pokerus = previousPokemon.pokerus;
// Transfer previous Pokemon's luck value
newPokemon.luck = previousPokemon.getLuck();
// If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock)
newPokemon.ivs = newPokemon.ivs.map((iv, index) => {
return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv;
});
// For pokemon that the player owns (including ones just caught), gain a candy
if (!forBattle && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1);
}
// Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species
newPokemon.generateAndPopulateMoveset();
// Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move
const newPokemonGeneratedMoveset = newPokemon.moveset;
newPokemon.moveset = previousPokemon.moveset.slice(0);
const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm, forBattle);
// Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset)
addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex);
// Randomize the second type of the pokemon
// If the pokemon does not normally have a second type, it will gain 1
const newTypes = [ newPokemon.getTypes()[0] ];
let newType = randSeedInt(18) as Type;
while (newType === newTypes[0]) {
newType = randSeedInt(18) as Type;
}
newTypes.push(newType);
if (!newPokemon.customPokemonData) {
newPokemon.customPokemonData = new CustomPokemonData();
}
newPokemon.customPokemonData.types = newTypes;
// Enable passive if previous had it
newPokemon.passive = previousPokemon.passive;
return isNewStarter;
}
/**
* @returns `true` if a given Pokemon has valid BST to be given an Old Gateau
*/
function shouldGetOldGateau(pokemon: Pokemon): boolean {
return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD;
}
/**
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
* @returns Array of 3 {@linkcode Stat}s to boost
*/
function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] {
const stats: Stat[] = [];
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
// HP or Speed
stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
return stats;
}
function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies {
let newSpecies: PokemonSpecies | undefined;
while (isNullOrUndefined(newSpecies)) {
@ -550,7 +685,7 @@ function doSideBySideTransformations(scene: BattleScene, transformations: Pokemo
* @param newPokemon
* @param speciesRootForm
*/
async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species): Promise<number | null> {
async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise<number | null> {
let eggMoveIndex: null | number = null;
const eggMoves = newPokemon.getEggMoves()?.slice(0);
if (eggMoves) {
@ -576,7 +711,7 @@ async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: Pla
}
// For pokemon that the player owns (including ones just caught), unlock the egg move
if (!isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
if (!forBattle && !isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true);
}
}

View File

@ -37,31 +37,58 @@ export abstract class EncounterSceneRequirement implements EncounterRequirement
abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string];
}
/**
* Combination of multiple {@linkcode EncounterSceneRequirement | EncounterSceneRequirements} (OR/AND possible. See {@linkcode isAnd})
*/
export class CombinationSceneRequirement extends EncounterSceneRequirement {
orRequirements: EncounterSceneRequirement[];
/** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */
private isAnd: boolean;
requirements: EncounterSceneRequirement[];
constructor(... orRequirements: EncounterSceneRequirement[]) {
public static Some(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement {
return new CombinationSceneRequirement(false, ...requirements);
}
public static Every(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement {
return new CombinationSceneRequirement(true, ...requirements);
}
private constructor(isAnd: boolean, ...requirements: EncounterSceneRequirement[]) {
super();
this.orRequirements = orRequirements;
this.isAnd = isAnd;
this.requirements = requirements;
}
/**
* Checks if all/any requirements are met (depends on {@linkcode isAnd})
* @param scene The {@linkcode BattleScene} to check against
* @returns true if all/any requirements are met (depends on {@linkcode isAnd})
*/
override meetsRequirement(scene: BattleScene): boolean {
for (const req of this.orRequirements) {
if (req.meetsRequirement(scene)) {
return true;
}
}
return false;
return this.isAnd
? this.requirements.every(req => req.meetsRequirement(scene))
: this.requirements.some(req => req.meetsRequirement(scene));
}
/**
* Retrieves a dialogue token key/value pair for the given {@linkcode EncounterSceneRequirement | requirements}.
* @param scene The {@linkcode BattleScene} to check against
* @param pokemon The {@linkcode PlayerPokemon} to check against
* @returns A dialogue token key/value pair
* @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported)
*/
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
for (const req of this.orRequirements) {
if (req.meetsRequirement(scene)) {
return req.getDialogueToken(scene, pokemon);
if (this.isAnd) {
throw new Error("Not implemented (Sorry)");
} else {
for (const req of this.requirements) {
if (req.meetsRequirement(scene)) {
return req.getDialogueToken(scene, pokemon);
}
}
}
return this.orRequirements[0].getDialogueToken(scene, pokemon);
return this.requirements[0].getDialogueToken(scene, pokemon);
}
}
}
@ -90,44 +117,74 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen
abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string];
}
/**
* Combination of multiple {@linkcode EncounterPokemonRequirement | EncounterPokemonRequirements} (OR/AND possible. See {@linkcode isAnd})
*/
export class CombinationPokemonRequirement extends EncounterPokemonRequirement {
orRequirements: EncounterPokemonRequirement[];
/** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */
private isAnd: boolean;
private requirements: EncounterPokemonRequirement[];
constructor(...orRequirements: EncounterPokemonRequirement[]) {
public static Some(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement {
return new CombinationPokemonRequirement(false, ...requirements);
}
public static Every(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement {
return new CombinationPokemonRequirement(true, ...requirements);
}
private constructor(isAnd: boolean, ...requirements: EncounterPokemonRequirement[]) {
super();
this.isAnd = isAnd;
this.invertQuery = false;
this.minNumberOfPokemon = 1;
this.orRequirements = orRequirements;
this.requirements = requirements;
}
/**
* Checks if all/any requirements are met (depends on {@linkcode isAnd})
* @param scene The {@linkcode BattleScene} to check against
* @returns true if all/any requirements are met (depends on {@linkcode isAnd})
*/
override meetsRequirement(scene: BattleScene): boolean {
for (const req of this.orRequirements) {
if (req.meetsRequirement(scene)) {
return true;
}
}
return false;
return this.isAnd
? this.requirements.every(req => req.meetsRequirement(scene))
: this.requirements.some(req => req.meetsRequirement(scene));
}
/**
* Queries the players party for all party members that are compatible with all/any requirements (depends on {@linkcode isAnd})
* @param partyPokemon The party of {@linkcode PlayerPokemon}
* @returns All party members that are compatible with all/any requirements (depends on {@linkcode isAnd})
*/
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
for (const req of this.orRequirements) {
const result = req.queryParty(partyPokemon);
if (result?.length > 0) {
return result;
}
if (this.isAnd) {
return this.requirements.reduce((relevantPokemon, req) => req.queryParty(relevantPokemon), partyPokemon);
} else {
const matchingRequirement = this.requirements.find(req => req.queryParty(partyPokemon).length > 0);
return matchingRequirement ? matchingRequirement.queryParty(partyPokemon) : [];
}
return [];
}
/**
* Retrieves a dialogue token key/value pair for the given {@linkcode EncounterPokemonRequirement | requirements}.
* @param scene The {@linkcode BattleScene} to check against
* @param pokemon The {@linkcode PlayerPokemon} to check against
* @returns A dialogue token key/value pair
* @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported)
*/
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
for (const req of this.orRequirements) {
if (req.meetsRequirement(scene)) {
return req.getDialogueToken(scene, pokemon);
if (this.isAnd) {
throw new Error("Not implemented (Sorry)");
} else {
for (const req of this.requirements) {
if (req.meetsRequirement(scene)) {
return req.getDialogueToken(scene, pokemon);
}
}
}
return this.orRequirements[0].getDialogueToken(scene, pokemon);
return this.requirements[0].getDialogueToken(scene, pokemon);
}
}
}

View File

@ -53,6 +53,7 @@ export interface IMysteryEncounter {
hasBattleAnimationsWithoutTargets: boolean;
skipEnemyBattleTurns: boolean;
skipToFightInput: boolean;
preventGameStatsUpdates: boolean;
onInit?: (scene: BattleScene) => boolean;
onVisualsStart?: (scene: BattleScene) => boolean;
@ -150,6 +151,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
* If true, will skip COMMAND input and go straight to FIGHT (move select) input menu
*/
skipToFightInput: boolean;
/**
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
*/
preventGameStatsUpdates: boolean;
// #region Event callback functions
@ -548,6 +553,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
hasBattleAnimationsWithoutTargets: boolean = false;
skipEnemyBattleTurns: boolean = false;
skipToFightInput: boolean = false;
preventGameStatsUpdates: boolean = false;
maxAllowedEncounters: number = 3;
expMultiplier: number = 1;
@ -735,6 +741,14 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return Object.assign(this, { skipToFightInput });
}
/**
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
* Default `false`
*/
withPreventGameStatsUpdates(preventGameStatsUpdates: boolean): this & Required<Pick<IMysteryEncounter, "preventGameStatsUpdates">> {
return Object.assign(this, { preventGameStatsUpdates });
}
/**
* Sets the maximum number of times that an encounter can spawn in a given Classic run
* @param maxAllowedEncounters

View File

@ -118,3 +118,20 @@ export const EXTORTION_ABILITIES = [
Abilities.SUCTION_CUPS,
Abilities.STICKY_HOLD
];
/**
* Abilities that signify resistance to fire
*/
export const FIRE_RESISTANT_ABILITIES = [
Abilities.FLAME_BODY,
Abilities.FLASH_FIRE,
Abilities.WELL_BAKED_BODY,
Abilities.HEATPROOF,
Abilities.THERMAL_EXCHANGE,
Abilities.THICK_FAT,
Abilities.WATER_BUBBLE,
Abilities.MAGMA_ARMOR,
Abilities.WATER_VEIL,
Abilities.STEAM_ENGINE,
Abilities.PRIMORDIAL_SEA
];

View File

@ -21,6 +21,8 @@ import { Gender } from "#app/data/gender";
import { PermanentStat } from "#enums/stat";
import { VictoryPhase } from "#app/phases/victory-phase";
import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Abilities } from "#enums/abilities";
/** Will give +1 level every 10 waves */
export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1;
@ -833,3 +835,21 @@ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scen
return null;
}
/**
* Permanently overrides the ability (not passive) of a pokemon.
* If the pokemon is a fusion, instead overrides the fused pokemon's ability.
*/
export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities) {
if (pokemon.isFusion()) {
if (!pokemon.fusionCustomPokemonData) {
pokemon.fusionCustomPokemonData = new CustomPokemonData();
}
pokemon.fusionCustomPokemonData.ability = ability;
} else {
if (!pokemon.customPokemonData) {
pokemon.customPokemonData = new CustomPokemonData();
}
pokemon.customPokemonData.ability = ability;
}
}

View File

@ -1864,7 +1864,7 @@ export function initSpecies() {
new PokemonSpecies(Species.ALOMOMOLA, 5, false, false, false, "Caring Pokémon", Type.WATER, null, 1.2, 31.6, Abilities.HEALER, Abilities.HYDRATION, Abilities.REGENERATOR, 470, 165, 75, 80, 40, 45, 65, 75, 70, 165, GrowthRate.FAST, 50, false),
new PokemonSpecies(Species.JOLTIK, 5, false, false, false, "Attaching Pokémon", Type.BUG, Type.ELECTRIC, 0.1, 0.6, Abilities.COMPOUND_EYES, Abilities.UNNERVE, Abilities.SWARM, 319, 50, 47, 50, 57, 50, 65, 190, 50, 64, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.GALVANTULA, 5, false, false, false, "EleSpider Pokémon", Type.BUG, Type.ELECTRIC, 0.8, 14.3, Abilities.COMPOUND_EYES, Abilities.UNNERVE, Abilities.SWARM, 472, 70, 77, 60, 97, 60, 108, 75, 50, 165, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.FERROSEED, 5, false, false, false, "Thorn Seed Pokémon", Type.GRASS, Type.STEEL, 0.6, 18.8, Abilities.IRON_BARBS, Abilities.NONE, Abilities.IRON_BARBS, 305, 44, 50, 91, 24, 86, 10, 255, 50, 61, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.FERROSEED, 5, false, false, false, "Thorn Seed Pokémon", Type.GRASS, Type.STEEL, 0.6, 18.8, Abilities.IRON_BARBS, Abilities.NONE, Abilities.ANTICIPATION, 305, 44, 50, 91, 24, 86, 10, 255, 50, 61, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.FERROTHORN, 5, false, false, false, "Thorn Pod Pokémon", Type.GRASS, Type.STEEL, 1, 110, Abilities.IRON_BARBS, Abilities.NONE, Abilities.ANTICIPATION, 489, 74, 94, 131, 54, 116, 20, 90, 50, 171, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.KLINK, 5, false, false, false, "Gear Pokémon", Type.STEEL, null, 0.3, 21, Abilities.PLUS, Abilities.MINUS, Abilities.CLEAR_BODY, 300, 40, 55, 70, 45, 60, 30, 130, 50, 60, GrowthRate.MEDIUM_SLOW, null, false),
new PokemonSpecies(Species.KLANG, 5, false, false, false, "Gear Pokémon", Type.STEEL, null, 0.6, 51, Abilities.PLUS, Abilities.MINUS, Abilities.CLEAR_BODY, 440, 60, 80, 95, 70, 85, 50, 60, 50, 154, GrowthRate.MEDIUM_SLOW, null, false),
@ -2580,11 +2580,11 @@ export function initSpecies() {
new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false,
new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true),
new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
),
new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false),
new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false),

View File

@ -1,4 +1,4 @@
import * as Utils from "../utils";
import { randIntRange } from "#app/utils";
import { StatusEffect } from "#enums/status-effect";
import i18next, { ParseKeys } from "i18next";
@ -6,17 +6,21 @@ export { StatusEffect };
export class Status {
public effect: StatusEffect;
public turnCount: integer;
public cureTurn: integer | null;
/** Toxic damage is `1/16 max HP * toxicTurnCount` */
public toxicTurnCount: number = 0;
public sleepTurnsRemaining?: number;
constructor(effect: StatusEffect, turnCount: integer = 0, cureTurn?: integer) {
constructor(effect: StatusEffect, toxicTurnCount: number = 0, sleepTurnsRemaining?: number) {
this.effect = effect;
this.turnCount = turnCount === undefined ? 0 : turnCount;
this.cureTurn = cureTurn!; // TODO: is this bang correct?
this.toxicTurnCount = toxicTurnCount;
this.sleepTurnsRemaining = sleepTurnsRemaining;
}
incrementTurn(): void {
this.turnCount++;
this.toxicTurnCount++;
if (this.sleepTurnsRemaining) {
this.sleepTurnsRemaining--;
}
}
isPostTurn(): boolean {
@ -107,7 +111,7 @@ export function getStatusEffectCatchRateMultiplier(statusEffect: StatusEffect):
* Returns a random non-volatile StatusEffect
*/
export function generateRandomStatusEffect(): StatusEffect {
return Utils.randIntRange(1, 6);
return randIntRange(1, 6);
}
/**
@ -123,7 +127,7 @@ export function getRandomStatusEffect(statusEffectA: StatusEffect, statusEffectB
return statusEffectA;
}
return Utils.randIntRange(0, 2) ? statusEffectA : statusEffectB;
return randIntRange(0, 2) ? statusEffectA : statusEffectB;
}
/**
@ -140,7 +144,7 @@ export function getRandomStatus(statusA: Status | null, statusB: Status | null):
}
return Utils.randIntRange(0, 2) ? statusA : statusB;
return randIntRange(0, 2) ? statusA : statusB;
}
/**

View File

@ -2500,6 +2500,22 @@ export const trainerConfigs: TrainerConfigs = {
[TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)),
[TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder")
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG))
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)),
[TrainerType.FUTURE_SELF_M]: new TrainerConfig(++t)
.setMoneyMultiplier(0)
.setEncounterBgm("mystery_encounter_weird_dream")
.setBattleBgm("mystery_encounter_weird_dream")
.setMixedBattleBgm("mystery_encounter_weird_dream")
.setVictoryBgm("mystery_encounter_weird_dream")
.setLocalizedName("Future Self M")
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)),
[TrainerType.FUTURE_SELF_F]: new TrainerConfig(++t)
.setMoneyMultiplier(0)
.setEncounterBgm("mystery_encounter_weird_dream")
.setBattleBgm("mystery_encounter_weird_dream")
.setMixedBattleBgm("mystery_encounter_weird_dream")
.setVictoryBgm("mystery_encounter_weird_dream")
.setLocalizedName("Future Self F")
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG))
};

View File

@ -116,6 +116,8 @@ export enum TrainerType {
VITO,
BUG_TYPE_SUPERFAN,
EXPERT_POKEMON_BREEDER,
FUTURE_SELF_M,
FUTURE_SELF_F,
BROCK = 200,
MISTY,

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives";
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
import { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "#app/data/ability";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, 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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability";
import PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui";
@ -93,7 +93,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public stats: integer[];
public ivs: integer[];
public nature: Nature;
public natureOverride: Nature | -1;
public moveset: (PokemonMove | null)[];
public status: Status | null;
public friendship: integer;
@ -2121,7 +2120,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Trainers get a weight bump to stat buffing moves
movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1) ]);
// Trainers get a weight decrease to multiturn moves
movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]);
movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]);
}
// Weight towards higher power moves, by reducing the power of moves below the highest power.
@ -2290,6 +2289,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate);
}
/**
* Compares if `this` and {@linkcode target} are on the same team.
* @param target the {@linkcode Pokemon} to compare against.
* @returns `true` if the two pokemon are allies, `false` otherwise
*/
public isOpponent(target: Pokemon): boolean {
return this.isPlayer() !== target.isPlayer();
}
getOpponent(targetIndex: integer): Pokemon | null {
const ret = this.getOpponents()[targetIndex];
if (ret.summonData) {
@ -2610,7 +2618,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */
const screenMultiplier = new Utils.NumberHolder(1);
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, moveCategory, screenMultiplier);
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, source, moveCategory, screenMultiplier);
/**
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
@ -3352,13 +3360,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
const types = this.getTypes(true, true);
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
return false;
}
const types = this.getTypes(true, true);
switch (effect) {
case StatusEffect.POISON:
case StatusEffect.TOXIC:
@ -3422,7 +3429,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return true;
}
trySetStatus(effect: StatusEffect | undefined, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, cureTurn: integer | null = 0, sourceText: string | null = null): boolean {
trySetStatus(effect?: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, turnsRemaining: number = 0, sourceText: string | null = null): boolean {
if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) {
return false;
}
@ -3436,15 +3443,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
if (asPhase) {
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon));
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, turnsRemaining, sourceText, sourcePokemon));
return true;
}
let statusCureTurn: Utils.IntegerHolder;
let sleepTurnsRemaining: Utils.NumberHolder;
if (effect === StatusEffect.SLEEP) {
statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4));
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, false, effect, statusCureTurn);
sleepTurnsRemaining = new Utils.NumberHolder(this.randSeedIntRange(2, 4));
this.setFrameRate(4);
@ -3464,9 +3470,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
statusCureTurn = statusCureTurn!; // tell TS compiler it's defined
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call
this.status = new Status(effect, 0, statusCureTurn?.value);
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
if (effect !== StatusEffect.FAINT) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
@ -3504,6 +3510,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
/**
* Checks if this Pokemon is protected by Safeguard
* @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon
* @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise.
*/
isSafeguarded(attacker: Pokemon): boolean {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
const bypassed = new Utils.BooleanHolder(false);
if (attacker) {
applyAbAttrs(InfiltratorAbAttr, attacker, null, false, bypassed);
}
return !bypassed.value;
}
return false;
}
primeSummonData(summonDataPrimer: PokemonSummonData): void {
this.summonDataPrimer = summonDataPrimer;
}
@ -3976,7 +3999,7 @@ export class PlayerPokemon extends Pokemon {
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
if (Overrides.STATUS_OVERRIDE) {
this.status = new Status(Overrides.STATUS_OVERRIDE);
this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4);
}
if (Overrides.SHINY_OVERRIDE) {
@ -4259,7 +4282,6 @@ export class PlayerPokemon extends Pokemon {
if (newEvolution.condition?.predicate(this)) {
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature);
newPokemon.natureOverride = this.natureOverride;
newPokemon.passive = this.passive;
newPokemon.moveset = this.moveset.slice();
newPokemon.moveset = this.copyMoveset();
@ -4409,7 +4431,7 @@ export class PlayerPokemon extends Pokemon {
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = this.scene.getParty().indexOf(this);
pokemon.getMoveset(true).map(m => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m!.getMove().id))); // TODO: is the bang correct?
pokemon.getMoveset(true).map((m: PokemonMove) => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id)));
pokemon.destroy();
this.updateFusionPalette();
resolve();
@ -4430,8 +4452,12 @@ export class PlayerPokemon extends Pokemon {
/** Returns a deep copy of this Pokemon's moveset array */
copyMoveset(): PokemonMove[] {
const newMoveset : PokemonMove[] = [];
this.moveset.forEach(move =>
newMoveset.push(new PokemonMove(move!.moveId, 0, move!.ppUp, move!.virtual))); // TODO: are those bangs correct?
this.moveset.forEach((move) => {
// TODO: refactor `moveset` to not accept `null`s
if (move) {
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual, move.maxPpOverride));
}
});
return newMoveset;
}
@ -4456,7 +4482,7 @@ export class EnemyPokemon extends Pokemon {
}
if (Overrides.OPP_STATUS_OVERRIDE) {
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE);
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4);
}
if (Overrides.OPP_GENDER_OVERRIDE) {
@ -4465,9 +4491,11 @@ export class EnemyPokemon extends Pokemon {
const speciesId = this.species.speciesId;
if (speciesId in Overrides.OPP_FORM_OVERRIDES
if (
speciesId in Overrides.OPP_FORM_OVERRIDES
&& Overrides.OPP_FORM_OVERRIDES[speciesId]
&& this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]) {
&& this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]
) {
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId] ?? 0;
}
@ -5153,15 +5181,22 @@ export interface DamageCalculationResult {
**/
export class PokemonMove {
public moveId: Moves;
public ppUsed: integer;
public ppUp: integer;
public ppUsed: number;
public ppUp: number;
public virtual: boolean;
constructor(moveId: Moves, ppUsed?: integer, ppUp?: integer, virtual?: boolean) {
/**
* If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform).
* This also nullifies all effects of `ppUp`.
*/
public maxPpOverride?: number;
constructor(moveId: Moves, ppUsed: number = 0, ppUp: number = 0, virtual: boolean = false, maxPpOverride?: number) {
this.moveId = moveId;
this.ppUsed = ppUsed || 0;
this.ppUp = ppUp || 0;
this.virtual = !!virtual;
this.ppUsed = ppUsed;
this.ppUp = ppUp;
this.virtual = virtual;
this.maxPpOverride = maxPpOverride;
}
/**
@ -5198,7 +5233,7 @@ export class PokemonMove {
}
getMovePp(): integer {
return this.getMove().pp + this.ppUp * Utils.toDmgValue(this.getMove().pp / 5);
return this.maxPpOverride || (this.getMove().pp + this.ppUp * Utils.toDmgValue(this.getMove().pp / 5));
}
getPpRatio(): number {
@ -5215,6 +5250,6 @@ export class PokemonMove {
* @return {PokemonMove} A valid pokemonmove object
*/
static loadMove(source: PokemonMove | any): PokemonMove {
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual);
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual, source.maxPpOverride);
}
}

View File

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

View File

@ -10,7 +10,9 @@ import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect
import { Type } from "#app/data/type";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier } from "#app/modifier/modifier";
import {
AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier
} from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier";
import Overrides from "#app/overrides";
import { Unlockables } from "#app/system/unlockables";
@ -382,7 +384,7 @@ export class PokemonPpUpModifierType extends PokemonMoveModifierType {
(_pokemon: PlayerPokemon) => {
return null;
}, (pokemonMove: PokemonMove) => {
if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3) {
if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3 || pokemonMove.maxPpOverride) {
return PartyUiHandler.NoEffectMessage;
}
return null;
@ -1561,6 +1563,7 @@ export const modifierTypes = {
VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1),
GOLDEN_POKEBALL: () => new ModifierType("modifierType:ModifierType.GOLDEN_POKEBALL", "pb_gold", (type, _args) => new ExtraModifierModifier(type), undefined, "se/pb_bounce_1"),
SILVER_POKEBALL: () => new ModifierType("modifierType:ModifierType.SILVER_POKEBALL", "pb_silver", (type, _args) => new TempExtraModifierModifier(type, 100), undefined, "se/pb_bounce_1"),
ENEMY_DAMAGE_BOOSTER: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", "wl_item_drop", (type, _args) => new EnemyDamageBoosterModifier(type, 5)),
ENEMY_DAMAGE_REDUCTION: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", "wl_guard_spec", (type, _args) => new EnemyDamageReducerModifier(type, 2.5)),
@ -1577,13 +1580,13 @@ export const modifierTypes = {
if (pregenArgs) {
return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number);
}
return new PokemonBaseStatTotalModifierType(randSeedInt(20));
return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1));
}),
MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) {
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]);
}
return new PokemonBaseStatFlatModifierType(randSeedInt(20), [ Stat.HP, Stat.ATK, Stat.DEF ]);
return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [ Stat.HP, Stat.ATK, Stat.DEF ]);
}),
MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) {

View File

@ -404,6 +404,14 @@ export abstract class LapsingPersistentModifier extends PersistentModifier {
this.battleCount = this.maxBattles;
}
/**
* Updates an existing modifier with a new `maxBattles` and `battleCount`.
*/
setNewBattleCount(count: number): void {
this.maxBattles = count;
this.battleCount = count;
}
getMaxBattles(): number {
return this.maxBattles;
}
@ -960,7 +968,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
this.stackCount = pokemon
? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length
: this.stackCount;
const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11);
@ -975,7 +983,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
getMaxHeldItemCount(pokemon: Pokemon): number {
this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length;
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length;
return 999;
}
}
@ -2158,7 +2166,7 @@ export class PokemonPpUpModifier extends ConsumablePokemonMoveModifier {
override apply(playerPokemon: PlayerPokemon): boolean {
const move = playerPokemon.getMoveset()[this.moveIndex];
if (move) {
if (move && !move.maxPpOverride) {
move.ppUp = Math.min(move.ppUp + this.upPoints, 3);
}
@ -3288,6 +3296,60 @@ export class ExtraModifierModifier extends PersistentModifier {
}
}
/**
* Modifier used for timed boosts to the player's shop item rewards.
* @extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class TempExtraModifierModifier extends LapsingPersistentModifier {
constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) {
super(type, maxBattles, battleCount, stackCount);
}
/**
* Goes through existing modifiers for any that match Silver Pokeball,
* which will then add the max count of the new item to the existing count of the current item.
* If no existing Silver Pokeballs are found, will add a new one.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param scene
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as TempExtraModifierModifier;
const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount();
modifierInstance.setNewBattleCount(newBattleCount);
scene.playSound("se/restore");
return true;
}
}
modifiers.push(this);
return true;
}
clone() {
return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
}
match(modifier: Modifier): boolean {
return (modifier instanceof TempExtraModifierModifier);
}
/**
* Increases the current rewards in the battle by the `stackCount`.
* @returns `true` if the shop reward number modifier applies successfully
* @param count {@linkcode NumberHolder} that holds the resulting shop item reward count
*/
apply(count: NumberHolder): boolean {
count.value += this.getStackCount();
return true;
}
}
export abstract class EnemyPersistentModifier extends PersistentModifier {
constructor(type: ModifierType, stackCount?: number) {
super(type, stackCount);

View File

@ -75,6 +75,8 @@ class DefaultOverrides {
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
/** Set to `true` to show all tutorials */
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
/** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */
readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null;
// ----------------
// PLAYER OVERRIDES

View File

@ -30,6 +30,15 @@ export class CommandPhase extends FieldPhase {
start() {
super.start();
const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND];
if (commandUiHandler) {
if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) {
commandUiHandler.setCursor(Command.FIGHT);
} else {
commandUiHandler.setCursor(commandUiHandler.getCursor());
}
}
if (this.fieldIndex) {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching

View File

@ -0,0 +1,84 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { MoveChargeAnim } from "#app/data/battle-anims";
import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/move";
import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon";
import { BooleanHolder } from "#app/utils";
import { MovePhase } from "#app/phases/move-phase";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveEndPhase } from "#app/phases/move-end-phase";
/**
* Phase for the "charging turn" of two-turn moves (e.g. Dig).
* @extends {@linkcode PokemonPhase}
*/
export class MoveChargePhase extends PokemonPhase {
/** The move instance that this phase applies */
public move: PokemonMove;
/** The field index targeted by the move (Charging moves assume single target) */
public targetIndex: BattlerIndex;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) {
super(scene, battlerIndex);
this.move = move;
this.targetIndex = targetIndex;
}
public override start() {
super.start();
const user = this.getUserPokemon();
const target = this.getTargetPokemon();
const move = this.move.getMove();
// If the target is somehow not defined, or the move is somehow not a ChargingMove,
// immediately end this phase.
if (!target || !(move.isChargingMove())) {
console.warn("Invalid parameters for MoveChargePhase");
return super.end();
}
new MoveChargeAnim(move.chargeAnim, move.id, user).play(this.scene, false, () => {
move.showChargeText(user, target);
applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => {
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
this.end();
});
});
}
/** Checks the move's instant charge conditions, then ends this phase. */
public override end() {
const user = this.getUserPokemon();
const move = this.move.getMove();
if (move.isChargingMove()) {
const instantCharge = new BooleanHolder(false);
applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge);
if (instantCharge.value) {
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed
this.scene.tryRemovePhase((phase) => phase instanceof MoveEndPhase && phase.getPokemon() === user);
// queue a new MovePhase for this move's attack phase
this.scene.unshiftPhase(new MovePhase(this.scene, user, [ this.targetIndex ], this.move, false));
} else {
user.getMoveQueue().push({ move: move.id, targets: [ this.targetIndex ]});
}
// Add this move's charging phase to the user's move history
user.pushMoveHistory({ move: this.move.moveId, targets: [ this.targetIndex ], result: MoveResult.OTHER });
}
super.end();
}
public getUserPokemon(): Pokemon {
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
}
public getTargetPokemon(): Pokemon | undefined {
return this.scene.getField(true).find((p) => this.targetIndex === p.getBattlerIndex());
}
}

View File

@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
@ -24,10 +24,10 @@ export class MoveEffectPhase extends PokemonPhase {
super(scene, battlerIndex);
this.move = move;
/**
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
* with no party members available to switch in, then the right Pokemon takes the index
* of the left Pokemon and gets hit unless this is checked.
*/
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
* with no party members available to switch in, then the right Pokemon takes the index
* of the left Pokemon and gets hit unless this is checked.
*/
if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
const i = targets.indexOf(battlerIndex);
targets.splice(i, i + 1);
@ -49,9 +49,9 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use.
*/
* Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use.
*/
const overridden = new Utils.BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove();
@ -66,10 +66,10 @@ export class MoveEffectPhase extends PokemonPhase {
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
/**
* If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/
* If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/
if (user.turnData.hitsLeft === -1) {
const hitCount = new Utils.IntegerHolder(1);
// Assume single target for multi hit
@ -86,16 +86,16 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
/**
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
* @see {@linkcode hitCheck}
*/
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
* @see {@linkcode hitCheck}
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
const hasActiveTargets = targets.some(t => t.isActive(true));
@ -104,11 +104,10 @@ export class MoveEffectPhase extends PokemonPhase {
&& !targets[0].getTag(SemiInvulnerableTag);
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
* (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase.
*/
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
* (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase.
*/
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit();
if (hasActiveTargets) {
@ -154,9 +153,9 @@ export class MoveEffectPhase extends PokemonPhase {
&& !target.getTag(SemiInvulnerableTag);
/**
* If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one).
*/
* If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one).
*/
if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) {
this.stopMultiHit(target);
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
@ -177,23 +176,23 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Since all fail/miss checks have applied, the move is considered successfully applied.
* It's worth noting that if the move has no effect or is protected against, this assignment
* is overwritten and the move is logged as a FAIL.
*/
* Since all fail/miss checks have applied, the move is considered successfully applied.
* It's worth noting that if the move has no effect or is protected against, this assignment
* is overwritten and the move is logged as a FAIL.
*/
moveHistoryEntry.result = MoveResult.SUCCESS;
/**
* Stores the result of applying the invoked move to the target.
* If the target is protected, the result is always `NO_EFFECT`.
* Otherwise, the hit result is based on type effectiveness, immunities,
* and other factors that may negate the attack or status application.
*
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated
* (for attack moves) and the target's HP is updated. However, this isn't
* made visible to the user until the resulting {@linkcode DamagePhase}
* is invoked.
*/
* Stores the result of applying the invoked move to the target.
* If the target is protected, the result is always `NO_EFFECT`.
* Otherwise, the hit result is based on type effectiveness, immunities,
* and other factors that may negate the attack or status application.
*
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated
* (for attack moves) and the target's HP is updated. However, this isn't
* made visible to the user until the resulting {@linkcode DamagePhase}
* is invoked.
*/
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
@ -211,9 +210,9 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* If the move has no effect on the target (i.e. the target is protected or immune),
* change the logged move result to FAIL.
*/
* If the move has no effect on the target (i.e. the target is protected or immune),
* change the logged move result to FAIL.
*/
if (hitResult === HitResult.NO_EFFECT) {
moveHistoryEntry.result = MoveResult.FAIL;
}
@ -222,43 +221,41 @@ export class MoveEffectPhase extends PokemonPhase {
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
/**
* If the user can change forms by using the invoked move,
* it only changes forms after the move's last hit
* (see Relic Song's interaction with Parental Bond when used by Meloetta).
*/
* If the user can change forms by using the invoked move,
* it only changes forms after the move's last hit
* (see Relic Song's interaction with Parental Bond when used by Meloetta).
*/
if (lastHit) {
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
}
/**
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs.
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result.
*/
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs.
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result.
*/
applyAttrs.push(new Promise(resolve => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move)
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT,
user, target, move).then(() => {
// All other effects require the move to not have failed or have been cancelled to trigger
if (hitResult !== HitResult.FAIL) {
/** Are the move's effects tied to the first turn of a charge move? */
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move));
/**
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects.
*/
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => {
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects.
*/
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
// All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
/**
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
@ -267,7 +264,7 @@ export class MoveEffectPhase extends PokemonPhase {
}
}
// If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
@ -280,17 +277,15 @@ export class MoveEffectPhase extends PokemonPhase {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING);
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
})).then(() => {
// Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
/**
* If the invoked move is an attack, apply the user's chance to
* steal an item from the target granted by Grip Claw
*/
* If the invoked move is an attack, apply the user's chance to
* steal an item from the target granted by Grip Claw
*/
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
@ -345,12 +340,12 @@ export class MoveEffectPhase extends PokemonPhase {
end() {
const user = this.getUserPokemon();
/**
* If this phase isn't for the invoked move's last strike,
* unshift another MoveEffectPhase for the next strike.
* Otherwise, queue a message indicating the number of times the move has struck
* (if the move has struck more than once), then apply the heal from Shell Bell
* to the user.
*/
* If this phase isn't for the invoked move's last strike,
* unshift another MoveEffectPhase for the next strike.
* Otherwise, queue a message indicating the number of times the move has struck
* (if the move has struck more than once), then apply the heal from Shell Bell
* to the user.
*/
if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase());
@ -449,9 +444,9 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed
*/
* Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed
*/
removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) {
@ -460,19 +455,19 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Prevents subsequent strikes of this phase's invoked move from occurring
* @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon
*/
* Prevents subsequent strikes of this phase's invoked move from occurring
* @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon
*/
stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */
if (target) {
this.removeTarget(target);
}
/**
* If no target specified, or the specified target was the last of this move's
* targets, completely cancel all subsequent strikes.
*/
* If no target specified, or the specified target was the last of this move's
* targets, completely cancel all subsequent strikes.
*/
if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here?
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here?

View File

@ -1,16 +1,17 @@
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr } from "#app/data/ability";
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability";
import { CommonAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#app/data/type";
import { getTerrainBlockMessage } from "#app/data/weather";
import { MoveUsedEvent } from "#app/events/battle-scene";
import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon";
import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
@ -22,6 +23,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next";
import { MoveChargePhase } from "#app/phases/move-charge-phase";
export class MovePhase extends BattlePhase {
protected _pokemon: Pokemon;
@ -134,6 +136,8 @@ export class MovePhase extends BattlePhase {
if (this.cancelled || this.failed) {
this.handlePreMoveFailures();
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
this.chargeMove();
} else {
this.useMove();
}
@ -168,25 +172,31 @@ export class MovePhase extends BattlePhase {
switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS:
if (!this.pokemon.randSeedInt(4)) {
activated = true;
this.cancelled = true;
}
activated = (!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
break;
case StatusEffect.SLEEP:
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn;
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this.pokemon, null, false, this.pokemon.status.effect, turnsRemaining);
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
this.cancelled = activated;
break;
case StatusEffect.FREEZE:
healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5);
healed =
!!this.move.getMove().findAttr((attr) =>
attr instanceof HealStatusEffectAttr
&& attr.selfTarget
&& attr.isOfEffect(StatusEffect.FREEZE))
|| (!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true)
|| Overrides.STATUS_ACTIVATION_OVERRIDE === false;
activated = !healed;
this.cancelled = activated;
break;
}
if (activated) {
this.cancel();
this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
} else if (healed) {
@ -219,12 +229,15 @@ export class MovePhase extends BattlePhase {
this.showMoveText();
// TODO: Clean up implementation of two-turn moves.
if (moveQueue.length > 0) {
// Using .shift here clears out two turn moves once they've been used
this.ignorePp = moveQueue.shift()?.ignorePP ?? false;
}
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
this.pokemon.lapseTag(BattlerTagType.CHARGING);
}
// "commit" to using the move, deducting PP.
if (!this.ignorePp) {
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
@ -288,6 +301,9 @@ export class MovePhase extends BattlePhase {
}
this.showFailedText(failedText);
// Remove the user from its semi-invulnerable state (if applicable)
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
}
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
@ -299,6 +315,35 @@ export class MovePhase extends BattlePhase {
}
}
/** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */
protected chargeMove() {
const move = this.move.getMove();
const targets = this.getActiveTargetPokemon();
if (move.applyConditions(this.pokemon, targets[0], move)) {
// Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
this.showMoveText();
this.scene.unshiftPhase(new MoveChargePhase(this.scene, this.pokemon.getBattlerIndex(), this.targets[0], this.move));
} else {
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
let failedText: string | undefined;
const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false));
if (failureMessage) {
failedText = failureMessage;
}
this.showMoveText();
this.showFailedText(failedText);
// Remove the user from its semi-invulnerable state (if applicable)
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
}
}
/**
* Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`,
* then ends the phase.
@ -412,8 +457,6 @@ export class MovePhase extends BattlePhase {
* - Lapses `AFTER_MOVE` tags:
* - This handles the effects of {@link Moves.SUBSTITUTE Substitute}
* - Removes the second turn of charge moves
*
* TODO: handle charge moves more gracefully
*/
protected handlePreMoveFailures(): void {
if (this.cancelled || this.failed) {
@ -445,18 +488,7 @@ export class MovePhase extends BattlePhase {
return;
}
if (this.move.getMove().hasAttr(ChargeAttr)) {
const lastMove = this.pokemon.getLastXMoves() as TurnMove[];
if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) {
this.scene.queueMessage(i18next.t("battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName()
}), 500);
return;
}
}
if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) {
if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) {
return;
}

View File

@ -8,26 +8,26 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonPhase } from "./pokemon-phase";
export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect | undefined;
private cureTurn?: integer | null;
private statusEffect?: StatusEffect;
private turnsRemaining?: number;
private sourceText?: string | null;
private sourcePokemon?: Pokemon | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, turnsRemaining?: number, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
super(scene, battlerIndex);
this.statusEffect = statusEffect;
this.cureTurn = cureTurn;
this.turnsRemaining = turnsRemaining;
this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect
this.sourcePokemon = sourcePokemon;
}
start() {
const pokemon = this.getPokemon();
if (pokemon && !pokemon.status) {
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (this.cureTurn) {
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
if (this.turnsRemaining) {
pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
}
pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => {

View File

@ -18,7 +18,7 @@ export class PostSummonPhase extends PokemonPhase {
const pokemon = this.getPokemon();
if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.turnCount = 0;
pokemon.status.toxicTurnCount = 0;
}
this.scene.arena.applyTags(ArenaTrapTag, false, pokemon);

View File

@ -30,7 +30,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
damage.value = Math.max(pokemon.getMaxHp() >> 3, 1);
break;
case StatusEffect.TOXIC:
damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1);
damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.toxicTurnCount), 1);
break;
case StatusEffect.BURN:
damage.value = Math.max(pokemon.getMaxHp() >> 4, 1);

View File

@ -1,7 +1,7 @@
import BattleScene from "#app/battle-scene";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type";
import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
@ -45,6 +45,7 @@ export class SelectModifierPhase extends BattlePhase {
const modifierCount = new Utils.IntegerHolder(3);
if (this.isPlayer()) {
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount);
}
// If custom modifiers are specified, overrides default item count
@ -274,7 +275,13 @@ export class SelectModifierPhase extends BattlePhase {
// Otherwise, continue with custom multiplier
multiplier = this.customModifierSettings.rerollMultiplier;
}
return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
const baseMultiplier = Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * (2 ** this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
// Apply Black Sludge to reroll cost
const modifiedRerollCost = new NumberHolder(baseMultiplier);
this.scene.applyModifier(HealShopCostModifier, true, modifiedRerollCost);
return modifiedRerollCost.value;
}
getPoolType(): ModifierPoolType {

View File

@ -64,7 +64,8 @@ export class StatStageChangePhase extends PokemonPhase {
const cancelled = new BooleanHolder(false);
if (!this.selfTarget && stages.value < 0) {
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, cancelled);
// TODO: add a reference to the source of the stat change to fix Infiltrator interaction
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, false, cancelled);
}
if (!cancelled.value && !this.selfTarget && stages.value < 0) {

View File

@ -65,8 +65,9 @@ export class SwitchSummonPhase extends SummonPhase {
const pokemon = this.getPokemon();
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
if (this.switchType === SwitchType.SWITCH) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
const substitute = pokemon.getTag(SubstituteTag);
if (substitute) {
this.scene.tweens.add({

View File

@ -25,12 +25,17 @@ export class VictoryPhase extends PokemonPhase {
start() {
super.start();
this.scene.gameData.gameStats.pokemonDefeated++;
const isMysteryEncounter = this.scene.currentBattle.isBattleMysteryEncounter();
// update Pokemon defeated count except for MEs that disable it
if (!isMysteryEncounter || !this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) {
this.scene.gameData.gameStats.pokemonDefeated++;
}
const expValue = this.getPokemon().getExpValue();
this.scene.applyPartyExp(expValue, true);
if (this.scene.currentBattle.isBattleMysteryEncounter()) {
if (isMysteryEncounter) {
handleMysteryEncounterVictory(this.scene, false, this.isExpOnly);
return this.end();
}

View File

@ -1569,6 +1569,10 @@ export class GameData {
}
setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void {
// Some Mystery Encounters block updates to these stats
if (this.scene.currentBattle?.isBattleMysteryEncounter() && this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) {
return;
}
const dexEntry = this.dexData[pokemon.species.speciesId];
dexEntry.seenAttr |= pokemon.getDexAttr();
if (incrementCount) {

View File

@ -92,7 +92,6 @@ export default class PokemonData {
this.stats = source.stats;
this.ivs = source.ivs;
this.nature = source.nature !== undefined ? source.nature : 0 as Nature;
this.natureOverride = source.natureOverride !== undefined ? source.natureOverride : -1;
this.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship;
this.metLevel = source.metLevel || 5;
this.metBiome = source.metBiome !== undefined ? source.metBiome : -1;
@ -117,6 +116,8 @@ export default class PokemonData {
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
// Deprecated, but needed for session data migration
this.natureOverride = source.natureOverride;
this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData);
this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData);
@ -134,10 +135,10 @@ export default class PokemonData {
}
}
} else {
this.moveset = (source.moveset || [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL) ]).filter(m => m).map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp));
this.moveset = (source.moveset || [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL) ]).filter(m => m).map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp, m.virtual, m.maxPpOverride));
if (!forHistory) {
this.status = source.status
? new Status(source.status.effect, source.status.turnCount, source.status.cureTurn)
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
: null;
}

View File

@ -0,0 +1,93 @@
import { Status } from "#app/data/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Early Bird", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.REST, Moves.BELLY_DRUM, Moves.SPLASH ])
.ability(Abilities.EARLY_BIRD)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("reduces Rest's sleep time to 1 turn", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.BELLY_DRUM);
await game.toNextTurn();
game.move.select(Moves.REST);
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("reduces 3-turn sleep to 1 turn", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.status = new Status(StatusEffect.SLEEP, 0, 4);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("reduces 1-turn sleep to 0 turns", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.status = new Status(StatusEffect.SLEEP, 0, 2);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
});

View File

@ -36,9 +36,7 @@ describe("Abilities - Imposter", () => {
});
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([
Species.DITTO
]);
await game.classicMode.startBattle([ Species.DITTO ]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
@ -62,25 +60,24 @@ describe("Abilities - Imposter", () => {
const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset();
expect(playerMoveset.length).toBe(enemyMoveset.length);
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
// TODO: Checks for 5 PP should be done here when that gets addressed
expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
}
const playerTypes = player.getTypes();
const enemyTypes = enemy.getTypes();
expect(playerTypes.length).toBe(enemyTypes.length);
for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
expect(playerTypes[i]).toBe(enemyTypes[i]);
}
}, 20000);
});
it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
await game.startBattle([
Species.DITTO
]);
await game.classicMode.startBattle([ Species.DITTO ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
@ -97,4 +94,26 @@ describe("Abilities - Imposter", () => {
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
});
it("should set each move's pp to a maximum of 5", async () => {
game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]);
await game.classicMode.startBattle([ Species.DITTO ]);
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
player.getMoveset().forEach(move => {
// Should set correct maximum PP without touching `ppUp`
if (move) {
if (move.moveId === Moves.SKETCH) {
expect(move.getMovePp()).toBe(1);
} else {
expect(move.getMovePp()).toBe(5);
}
expect(move.ppUp).toBe(0);
}
});
});
});

View File

@ -0,0 +1,107 @@
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
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("Abilities - Infiltrator", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.TACKLE, Moves.WATER_GUN, Moves.SPORE, Moves.BABY_DOLL_EYES ])
.ability(Abilities.INFILTRATOR)
.battleType("single")
.disableCrits()
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
it.each([
{ effectName: "Light Screen", tagType: ArenaTagType.LIGHT_SCREEN, move: Moves.WATER_GUN },
{ effectName: "Reflect", tagType: ArenaTagType.REFLECT, move: Moves.TACKLE },
{ effectName: "Aurora Veil", tagType: ArenaTagType.AURORA_VEIL, move: Moves.TACKLE }
])("should bypass the target's $effectName", async ({ tagType, move }) => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const preScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
game.scene.arena.addTag(tagType, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
const postScreenDmg = enemy.getAttackDamage(player, allMoves[move]).damage;
expect(postScreenDmg).toBe(preScreenDmg);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
it("should bypass the target's Safeguard", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
// TODO: fix this interaction to pass this test
it.skip("should bypass the target's Mist", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.scene.arena.addTag(ArenaTagType.MIST, 1, Moves.NONE, enemy.id, ArenaTagSide.ENEMY, true);
game.move.select(Moves.BABY_DOLL_EYES);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
it("should bypass the target's Substitute", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.addTag(BattlerTagType.SUBSTITUTE, 1, Moves.NONE, enemy.id);
game.move.select(Moves.BABY_DOLL_EYES);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.INFILTRATOR);
});
});

View File

@ -150,7 +150,7 @@ describe("Abilities - Magic Guard", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!;
const toxicStartCounter = enemyPokemon.status!.turnCount;
const toxicStartCounter = enemyPokemon.status!.toxicTurnCount;
//should be 0
await game.phaseInterceptor.to(TurnEndPhase);
@ -162,7 +162,7 @@ describe("Abilities - Magic Guard", () => {
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.status!.turnCount).toBeGreaterThan(toxicStartCounter);
expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter);
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
}
);

View File

@ -52,6 +52,7 @@ describe("Abilities - Volt Absorb", () => {
expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
});
it("should activate regardless of accuracy checks", async () => {
game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.SPLASH);
@ -71,6 +72,7 @@ describe("Abilities - Volt Absorb", () => {
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
});
it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => {
game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.DIVE);
@ -84,9 +86,7 @@ describe("Abilities - Volt Absorb", () => {
game.move.select(Moves.THUNDERBOLT);
enemyPokemon.hp = enemyPokemon.hp - 1;
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
});

View File

@ -1,8 +1,8 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
@ -31,7 +31,8 @@ describe("Arena - Gravity", () => {
.ability(Abilities.UNNERVE)
.enemyAbility(Abilities.BALL_FETCH)
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(Moves.SPLASH);
.enemyMoveset(Moves.SPLASH)
.enemyLevel(5);
});
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
@ -42,102 +43,121 @@ describe("Arena - Gravity", () => {
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn
await game.startBattle([ Species.PIKACHU ]);
await game.classicMode.startBattle([ Species.PIKACHU ]);
game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use non-OHKO move on second turn
await game.toNextTurn();
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 1.67);
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
});
it("OHKO move accuracy is not affected", async () => {
game.override.startingLevel(5);
game.override.enemyLevel(5);
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
const moveToCheck = allMoves[Moves.FISSURE];
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn
await game.startBattle([ Species.PIKACHU ]);
await game.classicMode.startBattle([ Species.PIKACHU ]);
game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use OHKO move on second turn
await game.toNextTurn();
game.move.select(Moves.FISSURE);
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30);
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
});
describe("Against flying types", () => {
it("can be hit by ground-type moves now", async () => {
game.override
.startingLevel(5)
.enemyLevel(5)
.enemySpecies(Species.PIDGEOT)
.moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]);
await game.startBattle([ Species.PIKACHU ]);
await game.classicMode.startBattle([ Species.PIKACHU ]);
const pidgeot = game.scene.getEnemyPokemon()!;
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
// Try earthquake on 1st turn (fails!);
game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(0);
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
// Setup Gravity on 2nd turn
await game.toNextTurn();
game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use ground move on 3rd turn
await game.toNextTurn();
game.move.select(Moves.EARTHQUAKE);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(1);
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
});
it("keeps super-effective moves super-effective after using gravity", async () => {
game.override
.startingLevel(5)
.enemyLevel(5)
.enemySpecies(Species.PIDGEOT)
.moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]);
await game.startBattle([ Species.PIKACHU ]);
await game.classicMode.startBattle([ Species.PIKACHU ]);
const pidgeot = game.scene.getEnemyPokemon()!;
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
// Setup Gravity on 1st turn
game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use electric move on 2nd turn
await game.toNextTurn();
game.move.select(Moves.THUNDERBOLT);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(2);
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2);
});
});
it("cancels Fly if its user is semi-invulnerable", async () => {
game.override
.enemySpecies(Species.SNORLAX)
.enemyMoveset(Moves.FLY)
.moveset([ Moves.GRAVITY, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.CHARIZARD ]);
const charizard = game.scene.getPlayerPokemon()!;
const snorlax = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
game.move.select(Moves.GRAVITY);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
await game.phaseInterceptor.to("TurnEndPhase");
expect(charizard.hp).toBe(charizard.getMaxHp());
});
});

View File

@ -1,4 +1,5 @@
import {
Status,
StatusEffect,
getStatusEffectActivationText,
getStatusEffectDescriptor,
@ -6,14 +7,19 @@ import {
getStatusEffectObtainText,
getStatusEffectOverlapText,
} from "#app/data/status-effect";
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 { mockI18next } from "#test/utils/testUtils";
import i18next from "i18next";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const pokemonName = "PKM";
const sourceText = "SOURCE";
describe("status-effect", () => {
describe("Status Effect Messages", () => {
beforeAll(() => {
i18next.init();
});
@ -299,3 +305,99 @@ describe("status-effect", () => {
vi.resetAllMocks();
});
});
describe("Status Effects", () => {
describe("Paralysis", () => {
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
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.moveset([ Moves.QUICK_ATTACK ])
.ability(Abilities.BALL_FETCH)
.statusEffect(StatusEffect.PARALYSIS);
});
it("causes the pokemon's move to fail when activated", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.QUICK_ATTACK);
await game.move.forceStatusActivation(true);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(true);
expect(game.scene.getPlayerPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
});
});
describe("Sleep", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should last the appropriate number of turns", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.status = new Status(StatusEffect.SLEEP, 0, 4);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status.effect).toBe(StatusEffect.SLEEP);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status.effect).toBe(StatusEffect.SLEEP);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status.effect).toBe(StatusEffect.SLEEP);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(player.status?.effect).toBeUndefined();
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
});
});

View File

@ -7,8 +7,6 @@ import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Items - Toxic orb", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
@ -27,10 +25,10 @@ describe("Items - Toxic orb", () => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.RATTATA)
.enemySpecies(Species.MAGIKARP)
.ability(Abilities.BALL_FETCH)
.enemyAbility(Abilities.BALL_FETCH)
.moveset([ Moves.SPLASH ])
.moveset(Moves.SPLASH)
.enemyMoveset(Moves.SPLASH)
.startingHeldItems([{
name: "TOXIC_ORB",
@ -39,22 +37,19 @@ describe("Items - Toxic orb", () => {
vi.spyOn(i18next, "t");
});
it("badly poisons the holder", async () => {
await game.classicMode.startBattle([ Species.MIGHTYENA ]);
it("should badly poison the holder", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const player = game.scene.getPlayerField()[0];
const player = game.scene.getPlayerPokemon()!;
expect(player.getHeldItems()[0].type.id).toBe("TOXIC_ORB");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
// Toxic orb should trigger here
await game.phaseInterceptor.run("MessagePhase");
await game.phaseInterceptor.to("MessagePhase");
expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything());
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.TOXIC);
// Damage should not have ticked yet.
expect(player.status?.turnCount).toBe(0);
}, TIMEOUT);
expect(player.status?.toxicTurnCount).toBe(0);
});
});

View File

@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) {
defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, move.category, multiplierHolder);
defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, attacker, move.category, multiplierHolder);
}
return move.power * multiplierHolder.value;

View File

@ -34,7 +34,7 @@ describe("Moves - Baton Pass", () => {
.disableCrits();
});
it("transfers all stat stages when player uses it", async() => {
it("transfers all stat stages when player uses it", async () => {
// arrange
await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]);
@ -91,7 +91,7 @@ describe("Moves - Baton Pass", () => {
]);
}, 20000);
it("doesn't transfer effects that aren't transferrable", async() => {
it("doesn't transfer effects that aren't transferrable", async () => {
game.override.enemyMoveset([ Moves.SALT_CURE ]);
await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]);
@ -106,4 +106,28 @@ describe("Moves - Baton Pass", () => {
expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined();
}, 20000);
it("doesn't allow binding effects from the user to persist", async () => {
game.override.moveset([ Moves.FIRE_SPIN, Moves.BATON_PASS ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FIRE_SPIN);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.toNextTurn();
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
game.move.select(Moves.BATON_PASS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
});
});

114
src/test/moves/dig.test.ts Normal file
View File

@ -0,0 +1,114 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest";
import GameManager from "#test/utils/gameManager";
describe("Moves - Dig", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.DIG)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
});
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined();
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
expect(playerDig?.ppUsed).toBe(1);
});
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
game.override.enemyAbility(Abilities.NO_GUARD);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("should not expend PP when the attack phase is cancelled", async () => {
game.override
.enemyAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPORE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
expect(playerDig?.ppUsed).toBe(0);
});
it("should cause the user to take double damage from Earthquake", async () => {
await game.classicMode.startBattle([ Species.DONDOZO ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
game.move.select(Moves.DIG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEffectPhase");
const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
// these hopefully get avoid rounding errors :shrug:
expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg);
expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1));
});
});

137
src/test/moves/dive.test.ts Normal file
View File

@ -0,0 +1,137 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
import { WeatherType } from "#enums/weather-type";
describe("Moves - Dive", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.DIVE)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
});
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeDefined();
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
expect(playerDive?.ppUsed).toBe(1);
});
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
game.override.enemyAbility(Abilities.NO_GUARD);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("should not expend PP when the attack phase is cancelled", async () => {
game.override
.enemyAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPORE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
expect(playerDive?.ppUsed).toBe(0);
});
it("should trigger on-contact post-defend ability effects", async () => {
game.override
.enemyAbility(Abilities.ROUGH_SKIN)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.battleData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN);
});
it("should cancel attack after Harsh Sunlight is set", async () => {
game.override.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("TurnStartPhase", false);
game.scene.arena.trySetWeather(WeatherType.HARSH_SUN, false);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
expect(playerDive?.ppUsed).toBe(1);
});
});

View File

@ -0,0 +1,104 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat";
import { WeatherType } from "#enums/weather-type";
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("Moves - Electro Shot", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.ELECTRO_SHOT)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should increase the user's Sp. Atk on the first turn, then attack on the second turn", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.ELECTRO_SHOT);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
expect(playerElectroShot?.ppUsed).toBe(1);
});
it.each([
{ weatherType: WeatherType.RAIN, name: "Rain" },
{ weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" }
])("should fully resolve in one turn if $name is active", async ({ weatherType }) => {
game.override.weather(weatherType);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.ELECTRO_SHOT);
await game.phaseInterceptor.to("MoveEffectPhase", false);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
expect(playerElectroShot?.ppUsed).toBe(1);
});
it("should only increase Sp. Atk once with Multi-Lens", async () => {
game.override
.weather(WeatherType.RAIN)
.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.ELECTRO_SHOT);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.turnData.hitCount).toBe(2);
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
});
});

122
src/test/moves/fly.test.ts Normal file
View File

@ -0,0 +1,122 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
describe("Moves - Fly", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.FLY)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.TACKLE);
vi.spyOn(allMoves[Moves.FLY], "accuracy", "get").mockReturnValue(100);
});
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined();
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
expect(playerFly?.ppUsed).toBe(1);
});
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
game.override.enemyAbility(Abilities.NO_GUARD);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
});
it("should not expend PP when the attack phase is cancelled", async () => {
game.override
.enemyAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPORE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.FLY);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
expect(playerFly?.ppUsed).toBe(0);
});
it("should be cancelled when another Pokemon uses Gravity", async () => {
game.override.enemyMoveset([ Moves.SPLASH, Moves.GRAVITY ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.FLY);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
await game.forceEnemyMove(Moves.GRAVITY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
expect(playerFly?.ppUsed).toBe(0);
});
});

View File

@ -0,0 +1,78 @@
import { EffectiveStat, Stat } from "#enums/stat";
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("Moves - Geomancy", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.GEOMANCY)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should boost the user's stats on the second turn of use", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ];
game.move.select(Moves.GEOMANCY);
await game.phaseInterceptor.to("TurnEndPhase");
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(0));
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
await game.phaseInterceptor.to("TurnEndPhase");
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2));
expect(player.getMoveHistory()).toHaveLength(2);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY);
expect(playerGeomancy?.ppUsed).toBe(1);
});
it("should execute over 2 turns between waves", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const player = game.scene.getPlayerPokemon()!;
const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ];
game.move.select(Moves.GEOMANCY);
await game.phaseInterceptor.to("MoveEndPhase", false);
await game.doKillOpponents();
await game.toNextWave();
await game.phaseInterceptor.to("TurnEndPhase");
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2));
expect(player.getMoveHistory()).toHaveLength(2);
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY);
expect(playerGeomancy?.ppUsed).toBe(1);
});
});

View File

@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) {
defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, move.category, multiplierHolder);
defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, attacker, move.category, multiplierHolder);
}
return move.power * multiplierHolder.value;

View File

@ -0,0 +1,52 @@
import { StatusEffect } from "#app/data/status-effect";
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 - Nightmare", () => {
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.RATTATA)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.enemyStatusEffect(StatusEffect.SLEEP)
.startingLevel(5)
.moveset([ Moves.NIGHTMARE, Moves.SPLASH ]);
});
it("lowers enemy hp by 1/4 each turn while asleep", async () => {
await game.classicMode.startBattle([ Species.HYPNO ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemyMaxHP = enemyPokemon.hp;
game.move.select(Moves.NIGHTMARE);
await game.toNextTurn();
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4));
// take a second turn to make sure damage occurs again
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4) - Math.floor(enemyMaxHP / 4));
});
});

View File

@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) {
defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, move.category, multiplierHolder);
defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, attacker, move.category, multiplierHolder);
}
return move.power * multiplierHolder.value;

View File

@ -0,0 +1,102 @@
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#enums/battler-tag-type";
import { WeatherType } from "#enums/weather-type";
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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Moves - Solar Beam", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset(Moves.SOLAR_BEAM)
.battleType("single")
.startingLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should deal damage in two turns if no weather is active", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SOLAR_BEAM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM);
expect(playerSolarBeam?.ppUsed).toBe(1);
});
it.each([
{ weatherType: WeatherType.SUNNY, name: "Sun" },
{ weatherType: WeatherType.HARSH_SUN, name: "Harsh Sun" }
])("should deal damage in one turn if $name is active", async ({ weatherType }) => {
game.override.weather(weatherType);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SOLAR_BEAM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM);
expect(playerSolarBeam?.ppUsed).toBe(1);
});
it.each([
{ weatherType: WeatherType.RAIN, name: "Rain" },
{ weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" }
])("should have its power halved in $name", async ({ weatherType }) => {
game.override.weather(weatherType);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const solarBeam = allMoves[Moves.SOLAR_BEAM];
vi.spyOn(solarBeam, "calculateBattlePower");
game.move.select(Moves.SOLAR_BEAM);
await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to("TurnEndPhase");
expect(solarBeam.calculateBattlePower).toHaveLastReturnedWith(60);
});
});

View File

@ -36,9 +36,7 @@ describe("Moves - Transform", () => {
});
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([
Species.DITTO
]);
await game.classicMode.startBattle([ Species.DITTO ]);
game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to(TurnEndPhase);
@ -62,25 +60,24 @@ describe("Moves - Transform", () => {
const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset();
expect(playerMoveset.length).toBe(enemyMoveset.length);
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) {
// TODO: Checks for 5 PP should be done here when that gets addressed
expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
}
const playerTypes = player.getTypes();
const enemyTypes = enemy.getTypes();
expect(playerTypes.length).toBe(enemyTypes.length);
for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
expect(playerTypes[i]).toBe(enemyTypes[i]);
}
}, 20000);
});
it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
await game.startBattle([
Species.DITTO
]);
await game.classicMode.startBattle([ Species.DITTO ]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
@ -97,4 +94,26 @@ describe("Moves - Transform", () => {
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
});
it("should set each move's pp to a maximum of 5", async () => {
game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]);
await game.classicMode.startBattle([ Species.DITTO ]);
const player = game.scene.getPlayerPokemon()!;
game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to(TurnEndPhase);
player.getMoveset().forEach(move => {
// Should set correct maximum PP without touching `ppUp`
if (move) {
if (move.moveId === Moves.SKETCH) {
expect(move.getMovePp()).toBe(1);
} else {
expect(move.getMovePp()).toBe(5);
}
expect(move.ppUp).toBe(0);
}
});
});
});

View File

@ -41,7 +41,8 @@ describe("Moves - Whirlwind", () => {
const staraptor = game.scene.getPlayerPokemon()!;
game.move.select(move);
await game.toNextTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);

View File

@ -0,0 +1,53 @@
import { BattlerIndex } from "#app/battle";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Will-O-Wisp", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.WILL_O_WISP, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should burn the opponent", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.WILL_O_WISP);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.toNextTurn();
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
});
});

View File

@ -206,7 +206,7 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(candyJarAfter?.stackCount).toBe(1);
});
it("Should remove Reviver Seed and give the player a Healing Charm", async () => {
it("Should remove Reviver Seed and give the player a Berry Pouch", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 1 Reviver Seed on party lead
@ -220,11 +220,11 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
expect(reviverSeedAfter).toBeUndefined();
expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(1);
expect(berryPouchAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1);
});
it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => {
@ -256,13 +256,13 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(shellBellAfter?.stackCount).toBe(1);
});
it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => {
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// 5 Healing Charms
// 3 Berry Pouches
scene.modifiers = [];
const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
healingCharm.stackCount = 5;
const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
healingCharm.stackCount = 3;
await scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Reviver Seed on party lead
@ -275,12 +275,12 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
expect(reviverSeedAfter).toBeUndefined();
expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(5);
expect(healingCharmAfter?.stackCount).toBe(3);
expect(shellBellAfter).toBeDefined();
expect(shellBellAfter?.stackCount).toBe(1);
});
@ -347,7 +347,7 @@ describe("Delibird-y - Mystery Encounter", () => {
});
});
it("Should decrease held item stacks and give the player a Berry Pouch", async () => {
it("Should decrease held item stacks and give the player a Healing Charm", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 2 Soul Dew on party lead
@ -361,14 +361,14 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
expect(soulDewAfter?.stackCount).toBe(1);
expect(berryPouchAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1);
expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(1);
});
it("Should remove held item and give the player a Berry Pouch", async () => {
it("Should remove held item and give the player a Healing Charm", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 1 Soul Dew on party lead
@ -382,20 +382,20 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
expect(soulDewAfter).toBeUndefined();
expect(berryPouchAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1);
expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(1);
});
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// 5 Healing Charms
scene.modifiers = [];
const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
healingCharm.stackCount = 3;
const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
healingCharm.stackCount = 5;
await scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Soul Dew on party lead
@ -408,12 +408,12 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
expect(soulDewAfter).toBeUndefined();
expect(berryPouchAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(3);
expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(5);
expect(shellBellAfter).toBeDefined();
expect(shellBellAfter?.stackCount).toBe(1);
});

View File

@ -12,7 +12,7 @@ import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encount
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { Type } from "#app/data/type";
import { Status, StatusEffect } from "#app/data/status-effect";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
@ -22,6 +22,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Abilities } from "#enums/abilities";
import i18next from "i18next";
const namespace = "mysteryEncounters/fieryFallout";
@ -42,10 +44,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves();
game.override.mysteryEncounterChance(100)
.startingWave(defaultWave)
.startingBiome(defaultBiome)
.disableTrainerWaves()
.moveset([ Moves.PAYBACK, Moves.THUNDERBOLT ]); // Required for attack type booster item generation
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
@ -109,12 +112,16 @@ describe("Fiery Fallout - Mystery Encounter", () => {
{
species: getPokemonSpecies(Species.VOLCARONA),
isBoss: false,
gender: Gender.MALE
gender: Gender.MALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: expect.any(Function)
},
{
species: getPokemonSpecies(Species.VOLCARONA),
isBoss: false,
gender: Gender.FEMALE
gender: Gender.FEMALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: expect.any(Function)
}
],
doubleBattle: true,
@ -157,12 +164,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(4);
expect(movePhases.length).toBe(2);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.FIRE_SPIN).length).toBe(2); // Fire spin used twice before battle
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.QUIVER_DANCE).length).toBe(2); // Quiver Dance used twice before battle
});
it("should give charcoal to lead pokemon", async () => {
it("should give attack type boosting item to lead pokemon", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
@ -172,8 +178,8 @@ describe("Fiery Fallout - Mystery Encounter", () => {
const leadPokemonId = scene.getParty()?.[0].id;
const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[];
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal");
expect(charcoal).toBeDefined;
const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier);
expect(item).toBeDefined;
});
});
@ -193,7 +199,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
});
});
it("should damage all non-fire party PKM by 20% and randomly burn 1", async () => {
it("should damage all non-fire party PKM by 20%, and burn + give Heatproof to a random Pokemon", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
const party = scene.getParty();
@ -210,7 +216,8 @@ describe("Fiery Fallout - Mystery Encounter", () => {
burnablePokemon.forEach((pkm) => {
expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2));
});
expect(burnablePokemon.some(pkm => pkm?.status?.effect === StatusEffect.BURN)).toBeTruthy();
expect(burnablePokemon.some(pkm => pkm.status?.effect === StatusEffect.BURN)).toBeTruthy();
expect(burnablePokemon.some(pkm => pkm.customPokemonData.ability === Abilities.HEATPROOF));
notBurnablePokemon.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp()));
});
@ -241,17 +248,15 @@ describe("Fiery Fallout - Mystery Encounter", () => {
});
});
it("should give charcoal to lead pokemon", async () => {
it("should give attack type boosting item to lead pokemon", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
await runMysteryEncounterToEnd(game, 3);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const leadPokemonId = scene.getParty()?.[0].id;
const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[];
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal");
expect(charcoal).toBeDefined;
const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[];
const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier);
expect(item).toBeDefined;
});
it("should leave encounter without battle", async () => {
@ -264,7 +269,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
});
it("should be disabled if not enough FIRE types are in party", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP, Species.ARCANINE ]);
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP ]);
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const encounterPhase = scene.getCurrentPhase();

View File

@ -86,7 +86,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([
{
levelAdditiveModifier: 1,
levelAdditiveModifier: 0.5,
disableSwitch: true,
pokemonConfigs: [
{

View File

@ -5,7 +5,7 @@ import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
@ -15,6 +15,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter";
import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { CommandPhase } from "#app/phases/command-phase";
import { ModifierTier } from "#app/modifier/modifier-tier";
const namespace = "mysteryEncounters/weirdDream";
const defaultParty = [ Species.MAGBY, Species.HAUNTER, Species.ABRA ];
@ -70,7 +72,7 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`);
expect(WeirdDreamEncounter.options.length).toBe(2);
expect(WeirdDreamEncounter.options.length).toBe(3);
});
it("should initialize fully", async () => {
@ -132,7 +134,7 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(plus40To50.length).toBe(1);
});
it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => {
it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in rewards", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 1);
await game.phaseInterceptor.to(SelectModifierPhase, false);
@ -141,11 +143,12 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(4);
expect(modifierSelectHandler.options.length).toEqual(5);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT");
});
it("should leave encounter without battle", async () => {
@ -158,7 +161,7 @@ describe("Weird Dream - Mystery Encounter", () => {
});
});
describe("Option 2 - Leave", () => {
describe("Option 2 - Battle Future Self", () => {
it("should have the correct properties", () => {
const option = WeirdDreamEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
@ -174,17 +177,63 @@ describe("Weird Dream - Mystery Encounter", () => {
});
});
it("should reduce party levels by 12.5%", async () => {
it("should start a battle against the player's transformation team", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 2, undefined, true);
const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(1);
expect(scene.getEnemyParty().length).toBe(scene.getParty().length);
});
it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 2, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(6);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE);
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE);
expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA);
expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA);
expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT);
expect(modifierSelectHandler.options[5].modifierTypeOption.type.tier - modifierSelectHandler.options[5].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT);
});
});
describe("Option 3 - Leave", () => {
it("should have the correct properties", () => {
const option = WeirdDreamEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`,
selected: [
{
text: `${namespace}:option.3.selected`,
},
],
});
});
it("should reduce party levels by 10%", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
const levelsPrior = scene.getParty().map(p => p.level);
await runMysteryEncounterToEnd(game, 2);
await runMysteryEncounterToEnd(game, 3);
const levelsAfter = scene.getParty().map(p => p.level);
for (let i = 0; i < levelsPrior.length; i++) {
expect(Math.max(Math.ceil(0.8875 * levelsPrior[i]), 1)).toBe(levelsAfter[i]);
expect(Math.max(Math.ceil(0.9 * levelsPrior[i]), 1)).toBe(levelsAfter[i]);
expect(scene.getParty()[i].levelExp).toBe(0);
}
@ -195,7 +244,7 @@ describe("Weird Dream - Mystery Encounter", () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 2);
await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});

View File

@ -1,12 +1,13 @@
import { BattlerIndex } from "#app/battle";
import { Moves } from "#app/enums/moves";
import Overrides from "#app/overrides";
import { CommandPhase } from "#app/phases/command-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
import { Moves } from "#enums/moves";
import { getMovePosition } from "#test/utils/gameManagerUtils";
import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper";
import { vi } from "vitest";
import { getMovePosition } from "../gameManagerUtils";
import { GameManagerHelper } from "./gameManagerHelper";
/**
* Helper to handle a Pokemon's move
@ -17,7 +18,7 @@ export class MoveHelper extends GameManagerHelper {
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`.
* Used to force a move to hit.
*/
async forceHit(): Promise<void> {
public async forceHit(): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
}
@ -26,9 +27,9 @@ export class MoveHelper extends GameManagerHelper {
* Intercepts {@linkcode MoveEffectPhase} and mocks the
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`.
* Used to force a move to miss.
* @param firstTargetOnly Whether the move should force miss on the first target only, in the case of multi-target moves.
* @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves.
*/
async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
public async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck");
@ -40,12 +41,12 @@ export class MoveHelper extends GameManagerHelper {
}
/**
* Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase}
* @param move the move to use
* @param pkmIndex the pokemon index. Relevant for double-battles only (defaults to 0)
* @param targetIndex The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required
*/
select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) {
* Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase}
* @param move - the move to use
* @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0)
* @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required
*/
public select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) {
const movePosition = getMovePosition(this.game.scene, pkmIndex, move);
this.game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
@ -59,4 +60,15 @@ export class MoveHelper extends GameManagerHelper {
this.game.selectTarget(movePosition, targetIndex);
}
}
/**
* Forces the Paralysis or Freeze status to activate on the next move by temporarily mocking {@linkcode Overrides.STATUS_ACTIVATION_OVERRIDE},
* advancing to the next `MovePhase`, and then resetting the override to `null`
* @param activated - `true` to force the status to activate, `false` to force the status to not activate (will cause Freeze to heal)
*/
public async forceStatusActivation(activated: boolean): Promise<void> {
vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activated);
await this.game.phaseInterceptor.to("MovePhase");
vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(null);
}
}

View File

@ -29,7 +29,7 @@ export class OverridesHelper extends GameManagerHelper {
* @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line
* @param biome the biome to set
*/
startingBiome(biome: Biome): this {
public startingBiome(biome: Biome): this {
this.game.scene.newArena(biome);
this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`);
return this;
@ -38,9 +38,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the starting wave (index)
* @param wave the wave (index) to set. Classic: `1`-`200`
* @returns this
* @returns `this`
*/
startingWave(wave: number): this {
public startingWave(wave: number): this {
vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave);
this.log(`Starting wave set to ${wave}!`);
return this;
@ -49,9 +49,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) starting level
* @param level the (pokemon) level to set
* @returns this
* @returns `this`
*/
startingLevel(level: Species | number): this {
public startingLevel(level: Species | number): this {
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(level);
this.log(`Player Pokemon starting level set to ${level}!`);
return this;
@ -62,7 +62,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param value the XP multiplier to set
* @returns `this`
*/
xpMultiplier(value: number): this {
public xpMultiplier(value: number): this {
vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value);
this.log(`XP Multiplier set to ${value}!`);
return this;
@ -71,9 +71,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) starting held items
* @param items the items to hold
* @returns this
* @returns `this`
*/
startingHeldItems(items: ModifierOverride[]) {
public startingHeldItems(items: ModifierOverride[]): this {
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
this.log("Player Pokemon starting held items set to:", items);
return this;
@ -82,9 +82,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) {@linkcode Species | species}
* @param species the (pokemon) {@linkcode Species | species} to set
* @returns this
* @returns `this`
*/
starterSpecies(species: Species | number): this {
public starterSpecies(species: Species | number): this {
vi.spyOn(Overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Player Pokemon species set to ${Species[species]} (=${species})!`);
return this;
@ -92,9 +92,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) to be a random fusion
* @returns this
* @returns `this`
*/
enableStarterFusion(): this {
public enableStarterFusion(): this {
vi.spyOn(Overrides, "STARTER_FUSION_OVERRIDE", "get").mockReturnValue(true);
this.log("Player Pokemon is a random fusion!");
return this;
@ -103,9 +103,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) fusion species
* @param species the fusion species to set
* @returns this
* @returns `this`
*/
starterFusionSpecies(species: Species | number): this {
public starterFusionSpecies(species: Species | number): this {
vi.spyOn(Overrides, "STARTER_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Player Pokemon fusion species set to ${Species[species]} (=${species})!`);
return this;
@ -114,9 +114,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemons) forms
* @param forms the (pokemon) forms to set
* @returns this
* @returns `this`
*/
starterForms(forms: Partial<Record<Species, number>>): this {
public starterForms(forms: Partial<Record<Species, number>>): this {
vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue(forms);
const formsStr = Object.entries(forms)
.map(([ speciesId, formIndex ]) => `${Species[speciesId]}=${formIndex}`)
@ -128,9 +128,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player's starting modifiers
* @param modifiers the modifiers to set
* @returns this
* @returns `this`
*/
startingModifier(modifiers: ModifierOverride[]): this {
public startingModifier(modifiers: ModifierOverride[]): this {
vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers);
this.log(`Player starting modifiers set to: ${modifiers}`);
return this;
@ -139,9 +139,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) {@linkcode Abilities | ability}
* @param ability the (pokemon) {@linkcode Abilities | ability} to set
* @returns this
* @returns `this`
*/
ability(ability: Abilities): this {
public ability(ability: Abilities): this {
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(ability);
this.log(`Player Pokemon ability set to ${Abilities[ability]} (=${ability})!`);
return this;
@ -150,9 +150,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) **passive** {@linkcode Abilities | ability}
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set
* @returns this
* @returns `this`
*/
passiveAbility(passiveAbility: Abilities): this {
public passiveAbility(passiveAbility: Abilities): this {
vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
this.log(`Player Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`);
return this;
@ -161,9 +161,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the player (pokemon) {@linkcode Moves | moves}set
* @param moveset the {@linkcode Moves | moves}set to set
* @returns this
* @returns `this`
*/
moveset(moveset: Moves | Moves[]): this {
public moveset(moveset: Moves | Moves[]): this {
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
if (!Array.isArray(moveset)) {
moveset = [ moveset ];
@ -178,7 +178,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set
* @returns
*/
statusEffect(statusEffect: StatusEffect): this {
public statusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
this.log(`Player Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
return this;
@ -186,9 +186,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override each wave to not have standard trainer battles
* @returns this
* @returns `this`
*/
disableTrainerWaves(): this {
public disableTrainerWaves(): this {
const realFn = getGameMode;
vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => {
const mode = realFn(gameMode);
@ -201,9 +201,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override each wave to not have critical hits
* @returns this
* @returns `this`
*/
disableCrits() {
public disableCrits(): this {
vi.spyOn(Overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true);
this.log("Critical hits are disabled!");
return this;
@ -212,9 +212,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the {@linkcode WeatherType | weather (type)}
* @param type {@linkcode WeatherType | weather type} to set
* @returns this
* @returns `this`
*/
weather(type: WeatherType): this {
public weather(type: WeatherType): this {
vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type);
this.log(`Weather set to ${Weather[type]} (=${type})!`);
return this;
@ -223,9 +223,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the seed
* @param seed the seed to set
* @returns this
* @returns `this`
*/
seed(seed: string): this {
public seed(seed: string): this {
vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => {
this.game.scene.waveSeed = seed;
Phaser.Math.RND.sow([ seed ]);
@ -239,9 +239,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the battle type (single or double)
* @param battleType battle type to set
* @returns this
* @returns `this`
*/
battleType(battleType: "single" | "double" | null): this {
public battleType(battleType: "single" | "double" | null): this {
vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType);
this.log(`Battle type set to ${battleType} only!`);
return this;
@ -250,9 +250,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) {@linkcode Species | species}
* @param species the (pokemon) {@linkcode Species | species} to set
* @returns this
* @returns `this`
*/
enemySpecies(species: Species | number): this {
public enemySpecies(species: Species | number): this {
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Enemy Pokemon species set to ${Species[species]} (=${species})!`);
return this;
@ -260,9 +260,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) to be a random fusion
* @returns this
* @returns `this`
*/
enableEnemyFusion(): this {
public enableEnemyFusion(): this {
vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true);
this.log("Enemy Pokemon is a random fusion!");
return this;
@ -271,9 +271,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) fusion species
* @param species the fusion species to set
* @returns this
* @returns `this`
*/
enemyFusionSpecies(species: Species | number): this {
public enemyFusionSpecies(species: Species | number): this {
vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Enemy Pokemon fusion species set to ${Species[species]} (=${species})!`);
return this;
@ -282,9 +282,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) {@linkcode Abilities | ability}
* @param ability the (pokemon) {@linkcode Abilities | ability} to set
* @returns this
* @returns `this`
*/
enemyAbility(ability: Abilities): this {
public enemyAbility(ability: Abilities): this {
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
this.log(`Enemy Pokemon ability set to ${Abilities[ability]} (=${ability})!`);
return this;
@ -293,9 +293,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) **passive** {@linkcode Abilities | ability}
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set
* @returns this
* @returns `this`
*/
enemyPassiveAbility(passiveAbility: Abilities): this {
public enemyPassiveAbility(passiveAbility: Abilities): this {
vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
this.log(`Enemy Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`);
return this;
@ -304,9 +304,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) {@linkcode Moves | moves}set
* @param moveset the {@linkcode Moves | moves}set to set
* @returns this
* @returns `this`
*/
enemyMoveset(moveset: Moves | Moves[]): this {
public enemyMoveset(moveset: Moves | Moves[]): this {
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
if (!Array.isArray(moveset)) {
moveset = [ moveset ];
@ -319,9 +319,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) level
* @param level the level to set
* @returns this
* @returns `this`
*/
enemyLevel(level: number): this {
public enemyLevel(level: number): this {
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level);
this.log(`Enemy Pokemon level set to ${level}!`);
return this;
@ -332,7 +332,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set
* @returns
*/
enemyStatusEffect(statusEffect: StatusEffect): this {
public enemyStatusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
return this;
@ -341,9 +341,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (pokemon) held items
* @param items the items to hold
* @returns this
* @returns `this`
*/
enemyHeldItems(items: ModifierOverride[]) {
public enemyHeldItems(items: ModifierOverride[]): this {
vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
this.log("Enemy Pokemon held items set to:", items);
return this;
@ -354,7 +354,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param unlockable The Unlockable(s) to enable.
* @returns `this`
*/
enableUnlockable(unlockable: Unlockables[]) {
public enableUnlockable(unlockable: Unlockables[]): this {
vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable);
this.log("Temporarily unlocked the following content: ", unlockable);
return this;
@ -363,9 +363,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the items rolled at the end of a battle
* @param items the items to be rolled
* @returns this
* @returns `this`
*/
itemRewards(items: ModifierOverride[]) {
public itemRewards(items: ModifierOverride[]): this {
vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items);
this.log("Item rewards set to:", items);
return this;
@ -375,8 +375,9 @@ export class OverridesHelper extends GameManagerHelper {
* Override player shininess
* @param shininess - `true` or `false` to force the player's pokemon to be shiny or not shiny,
* `null` to disable the override and re-enable RNG shinies.
* @returns `this`
*/
shiny(shininess: boolean | null): this {
public shiny(shininess: boolean | null): this {
vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess);
if (shininess === null) {
this.log("Disabled player Pokemon shiny override!");
@ -389,8 +390,9 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override player shiny variant
* @param variant - The player's shiny variant.
* @returns `this`
*/
shinyVariant(variant: Variant): this {
public shinyVariant(variant: Variant): this {
vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant);
this.log(`Set player Pokemon's shiny variant to ${variant}!`);
return this;
@ -420,23 +422,38 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the enemy (Pokemon) to have the given amount of health segments
* @param healthSegments the number of segments to give
* default: 0, the health segments will be handled like in the game based on wave, level and species
* 1: the Pokemon will not be a boss
* 2+: the Pokemon will be a boss with the given number of health segments
* @returns this
* - `0` (default): the health segments will be handled like in the game based on wave, level and species
* - `1`: the Pokemon will not be a boss
* - `2`+: the Pokemon will be a boss with the given number of health segments
* @returns `this`
*/
enemyHealthSegments(healthSegments: number) {
public enemyHealthSegments(healthSegments: number): this {
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
this.log("Enemy Pokemon health segments set to:", healthSegments);
return this;
}
/**
* Override statuses (Paralysis and Freeze) to always or never activate
* @param activate - `true` to force activation, `false` to force no activation, `null` to disable the override
* @returns `this`
*/
public statusActivation(activate: boolean | null): this {
vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activate);
if (activate !== null) {
this.log(`Paralysis and Freeze forced to ${activate ? "always" : "never"} activate!`);
} else {
this.log("Status activation override disabled!");
}
return this;
}
/**
* Override the encounter chance for a mystery encounter.
* @param percentage the encounter chance in %
* @returns spy instance
* @returns `this`
*/
mysteryEncounterChance(percentage: number) {
public mysteryEncounterChance(percentage: number): this {
const maxRate: number = 256; // 100%
const rate = maxRate * (percentage / 100);
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate);
@ -446,10 +463,10 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the encounter chance for a mystery encounter.
* @returns spy instance
* @param tier
* @param tier - The {@linkcode MysteryEncounterTier} to encounter
* @returns `this`
*/
mysteryEncounterTier(tier: MysteryEncounterTier) {
public mysteryEncounterTier(tier: MysteryEncounterTier): this {
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier);
this.log(`Mystery encounter tier set to ${tier}!`);
return this;
@ -457,10 +474,10 @@ export class OverridesHelper extends GameManagerHelper {
/**
* Override the encounter that spawns for the scene
* @param encounterType
* @returns spy instance
* @param encounterType - The {@linkcode MysteryEncounterType} of the encounter
* @returns `this`
*/
mysteryEncounter(encounterType: MysteryEncounterType) {
public mysteryEncounter(encounterType: MysteryEncounterType): this {
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType);
this.log(`Mystery encounter override set to ${encounterType}!`);
return this;

View File

@ -2,37 +2,83 @@ import BattleScene from "#app/battle-scene";
import { ModalConfig } from "./modal-ui-handler";
import { Mode } from "./ui";
import * as Utils from "../utils";
import { FormModalUiHandler } from "./form-modal-ui-handler";
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
import { Button } from "#app/enums/buttons";
import { TextStyle } from "./text";
export default class AdminUiHandler extends FormModalUiHandler {
private adminMode: AdminMode;
private adminResult: AdminSearchInfo;
private config: ModalConfig;
private readonly buttonGap = 10;
// http response from the server when a username isn't found in the server
private readonly httpUserNotFoundErrorCode: number = 404;
private readonly ERR_REQUIRED_FIELD = (field: string) => {
if (field === "username") {
return `${Utils.formatText(field)} is required`;
} else {
return `${Utils.formatText(field)} Id is required`;
}
};
// returns a string saying whether a username has been successfully linked/unlinked to discord/google
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => {
return `Username and ${service} successfully ${mode.toLowerCase()}ed`;
};
private readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!";
private readonly ERR_GENERIC_ERROR: string = "There was an error";
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
}
setup(): void {
super.setup();
}
getModalTitle(config?: ModalConfig): string {
override getModalTitle(): string {
return "Admin panel";
}
getFields(config?: ModalConfig): string[] {
return [ "Username", "Discord ID" ];
override getWidth(): number {
return this.adminMode === AdminMode.ADMIN ? 180 : 160;
}
getWidth(config?: ModalConfig): number {
return 160;
override getMargin(): [number, number, number, number] {
return [ 0, 0, 0, 0 ];
}
getMargin(config?: ModalConfig): [number, number, number, number] {
return [ 0, 0, 48, 0 ];
override getButtonLabels(): string[] {
switch (this.adminMode) {
case AdminMode.LINK:
return [ "Link Account", "Cancel" ];
case AdminMode.SEARCH:
return [ "Find account", "Cancel" ];
case AdminMode.ADMIN:
return [ "Back to search", "Cancel" ];
default:
return [ "Activate ADMIN", "Cancel" ];
}
}
getButtonLabels(config?: ModalConfig): string[] {
return [ "Link account", "Cancel" ];
override getInputFieldConfigs(): InputFieldConfig[] {
const inputFieldConfigs: InputFieldConfig[] = [];
switch (this.adminMode) {
case AdminMode.LINK:
inputFieldConfigs.push( { label: "Username" });
inputFieldConfigs.push( { label: "Discord ID" });
break;
case AdminMode.SEARCH:
inputFieldConfigs.push( { label: "Username" });
break;
case AdminMode.ADMIN:
const adminResult = this.adminResult ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" };
// Discord and Google ID fields that are not empty get locked, other fields are all locked
inputFieldConfigs.push( { label: "Username", isReadOnly: true });
inputFieldConfigs.push( { label: "Discord ID", isReadOnly: adminResult.discordId !== "" });
inputFieldConfigs.push( { label: "Google ID", isReadOnly: adminResult.googleId !== "" });
inputFieldConfigs.push( { label: "Last played", isReadOnly: true });
inputFieldConfigs.push( { label: "Registered", isReadOnly: true });
break;
}
return inputFieldConfigs;
}
processInput(button: Button): boolean {
@ -45,44 +91,281 @@ export default class AdminUiHandler extends FormModalUiHandler {
}
show(args: any[]): boolean {
this.config = args[0] as ModalConfig; // config
this.adminMode = args[1] as AdminMode; // admin mode
this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; // admin result, if any
const isMessageError = args[3]; // is the message shown a success or error
const fields = this.getInputFieldConfigs();
const hasTitle = !!this.getModalTitle();
this.updateFields(fields, hasTitle);
this.updateContainer(this.config);
const labels = this.getButtonLabels();
for (let i = 0; i < labels.length; i++) {
this.buttonLabels[i].setText(labels[i]); // sets the label text
}
this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); // sets the position of the message dynamically
if (isMessageError) {
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
} else {
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_GREEN));
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_GREEN, true));
}
if (super.show(args)) {
const config = args[0] as ModalConfig;
this.populateFields(this.adminMode, this.adminResult);
const originalSubmitAction = this.submitAction;
this.submitAction = (_) => {
this.submitAction = originalSubmitAction;
const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later
const validFields = this.areFieldsValid(this.adminMode);
if (validFields.error) {
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
return this.showMessage(validFields.errorMessage ?? "", adminSearchResult, true);
}
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
const onFail = error => {
this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() }));
this.scene.ui.playError();
};
if (!this.inputs[0].text) {
return onFail("Username is required");
if (this.adminMode === AdminMode.LINK) {
this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord
.then(response => {
if (response.error) {
return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind
} else {
return this.showMessage(this.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success
}
});
} else if (this.adminMode === AdminMode.SEARCH) {
this.adminSearch(adminSearchResult) // admin search for username
.then(response => {
if (response.error) {
return this.showMessage(response.errorType, adminSearchResult, true); // failure
}
this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success
});
} else if (this.adminMode === AdminMode.ADMIN) {
this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH);
}
if (!this.inputs[1].text) {
return onFail("Discord Id is required");
}
Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true)
.then(response => {
if (!response.ok) {
console.error(response);
}
this.inputs[0].setText("");
this.inputs[1].setText("");
this.scene.ui.revertMode();
})
.catch((err) => {
console.error(err);
this.scene.ui.revertMode();
});
return false;
};
return true;
}
return false;
}
showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) {
this.scene.ui.setMode(Mode.ADMIN, Object.assign(this.config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError);
if (isError) {
this.scene.ui.playError();
} else {
this.scene.ui.playSelect();
}
}
/**
* This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult}
* to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord.
* For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the
* appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result
*/
private populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) {
switch (adminMode) {
case AdminMode.LINK:
this.inputs[0].setText(adminResult.username);
this.inputs[1].setText(adminResult.discordId);
break;
case AdminMode.SEARCH:
this.inputs[0].setText(adminResult.username);
break;
case AdminMode.ADMIN:
Object.keys(adminResult).forEach((aR, i) => {
this.inputs[i].setText(adminResult[aR]);
if (aR === "discordId" || aR === "googleId") { // this is here to add the icons for linking/unlinking of google/discord IDs
const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice");
const img = this.scene.add.image(this.inputContainers[i].x + nineSlice!.width + this.buttonGap, this.inputContainers[i].y + (Math.floor(nineSlice!.height / 2)), adminResult[aR] === "" ? "link_icon" : "unlink_icon");
img.setName(`adminBtn_${aR}`);
img.setOrigin(0.5, 0.5);
img.setInteractive();
img.on("pointerdown", () => {
const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly
const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service
const validFields = this.areFieldsValid(this.adminMode, service);
if (validFields.error) {
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
return this.showMessage(validFields.errorMessage ?? "", adminResult, true);
}
this.adminLinkUnlink(this.convertInputsToAdmin(), service, mode).then(response => { // attempts to link/unlink depending on the service
if (response.error) {
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
return this.showMessage(response.errorType, adminResult, true); // fail
} else { // success, reload panel with new results
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
this.adminSearch(adminResult)
.then(response => {
if (response.error) {
return this.showMessage(response.errorType, adminResult, true);
}
return this.showMessage(this.SUCCESS_SERVICE_MODE(service, mode), response.adminSearchResult ?? adminResult, false);
});
}
});
});
this.addInteractionHoverEffect(img);
this.modalContainer.add(img);
}
});
break;
}
}
private areFieldsValid(adminMode: AdminMode, service?: string): { error: boolean; errorMessage?: string; } {
switch (adminMode) {
case AdminMode.LINK:
if (!this.inputs[0].text) { // username missing from link panel
return {
error: true,
errorMessage: this.ERR_REQUIRED_FIELD("username")
};
}
if (!this.inputs[1].text) { // discordId missing from linking panel
return {
error: true,
errorMessage: this.ERR_REQUIRED_FIELD("discord")
};
}
break;
case AdminMode.SEARCH:
if (!this.inputs[0].text) { // username missing from search panel
return {
error: true,
errorMessage: this.ERR_REQUIRED_FIELD("username")
};
}
break;
case AdminMode.ADMIN:
if (!this.inputs[1].text && service === "discord") { // discordId missing from admin panel
return {
error: true,
errorMessage: this.ERR_REQUIRED_FIELD(service)
};
}
if (!this.inputs[2].text && service === "google") { // googleId missing from admin panel
return {
error: true,
errorMessage: this.ERR_REQUIRED_FIELD(service)
};
}
break;
}
return {
error: false
};
}
private convertInputsToAdmin(): AdminSearchInfo {
return {
username: this.inputs[0]?.node ? this.inputs[0].text : "",
discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "",
googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "",
lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "",
registered: this.inputs[4]?.node ? this.inputs[4]?.text : ""
};
}
private async adminSearch(adminSearchResult: AdminSearchInfo) {
try {
const adminInfo = await Utils.apiFetch(`admin/account/adminSearch?username=${encodeURIComponent(adminSearchResult.username)}`, true);
if (!adminInfo.ok) { // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
return { adminSearchResult: adminSearchResult, error: true, errorType: adminInfo.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR };
} else { // success
const adminInfoJson: AdminSearchInfo = await adminInfo.json();
return { adminSearchResult: adminInfoJson, error: false };
}
} catch (err) {
console.error(err);
return { error: true, errorType: err };
}
}
private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) {
try {
const response = await Utils.apiPost(`admin/account/${service}${mode}`, `username=${encodeURIComponent(adminSearchResult.username)}&${service}Id=${encodeURIComponent(service === "discord" ? adminSearchResult.discordId : adminSearchResult.googleId)}`, "application/x-www-form-urlencoded", true);
if (!response.ok) { // error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
return { adminSearchResult: adminSearchResult, error: true, errorType: response.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR };
} else { // success!
return { adminSearchResult: adminSearchResult, error: false };
}
} catch (err) {
console.error(err);
return { error: true, errorType: err };
}
}
private updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) {
mode = mode ?? AdminMode.ADMIN;
this.scene.ui.setMode(Mode.ADMIN, {
buttonActions: [
// we double revert here and below to go back 2 layers of menus
() => {
this.scene.ui.revertMode();
this.scene.ui.revertMode();
},
() => {
this.scene.ui.revertMode();
this.scene.ui.revertMode();
}
]
}, mode, adminSearchResult);
}
clear(): void {
super.clear();
// this is used to remove the existing fields on the admin panel so they can be updated
const itemsToRemove: string[] = [ "formLabel", "adminBtn" ]; // this is the start of the names for each element we want to remove
const removeArray: any[] = [];
const mC = this.modalContainer.list;
for (let i = mC.length - 1; i >= 0; i--) {
/* This code looks for a few things before destroying the specific field; first it looks to see if the name of the element is %like% the itemsToRemove labels
* this means that anything with, for example, "formLabel", will be true.
* It then also checks for any containers that are within this.modalContainer, and checks if any of its child elements are of type rexInputText
* and if either of these conditions are met, the element is destroyed.
*/
if (itemsToRemove.some(iTR => mC[i].name.includes(iTR)) || (mC[i].type === "Container" && (mC[i] as Phaser.GameObjects.Container).list.find(m => m.type === "rexInputText"))) {
removeArray.push(mC[i]);
}
}
while (removeArray.length > 0) {
this.modalContainer.remove(removeArray.pop(), true);
}
}
}
export enum AdminMode {
LINK,
SEARCH,
ADMIN
}
export function getAdminModeName(adminMode: AdminMode): string {
switch (adminMode) {
case AdminMode.LINK:
return "Link";
case AdminMode.SEARCH:
return "Search";
default:
return "";
}
}
interface AdminSearchInfo {
username: string;
discordId: string;
googleId: string;
lastLoggedIn: string;
registered: string;
}

View File

@ -5,7 +5,6 @@ import { TextStyle, addTextInputObject, addTextObject } from "./text";
import { WindowVariant, addWindow } from "./ui-theme";
import InputText from "phaser3-rex-plugins/plugins/inputtext";
import * as Utils from "../utils";
import i18next from "i18next";
import { Button } from "#enums/buttons";
export interface FormModalConfig extends ModalConfig {
@ -19,6 +18,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
protected errorMessage: Phaser.GameObjects.Text;
protected submitAction: Function | null;
protected tween: Phaser.Tweens.Tween;
protected formLabels: Phaser.GameObjects.Text[];
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
@ -26,12 +26,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
this.editing = false;
this.inputContainers = [];
this.inputs = [];
this.formLabels = [];
}
abstract getFields(): string[];
/**
* Get configuration for all fields that should be part of the modal
* Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view
* @returns array of {@linkcode InputFieldConfig}
*/
abstract getInputFieldConfigs(): InputFieldConfig[];
getHeight(config?: ModalConfig): number {
return 20 * this.getFields().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28;
return 20 * this.getInputFieldConfigs().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28;
}
getReadableErrorMessage(error: string): string {
@ -45,37 +51,50 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
setup(): void {
super.setup();
const fields = this.getFields();
const config = this.getInputFieldConfigs();
const hasTitle = !!this.getModalTitle();
fields.forEach((field, f) => {
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT);
if (config.length >= 1) {
this.updateFields(config, hasTitle);
}
this.modalContainer.add(label);
this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT);
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
this.errorMessage.setVisible(false);
this.modalContainer.add(this.errorMessage);
}
protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) {
this.inputContainers = [];
this.inputs = [];
this.formLabels = [];
fieldsConfig.forEach((config, f) => {
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT);
label.name = "formLabel" + f;
this.formLabels.push(label);
this.modalContainer.add(this.formLabels[this.formLabels.length - 1]);
const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
inputContainer.setVisible(false);
const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN);
const isPassword = field.includes(i18next.t("menu:password")) || field.includes(i18next.t("menu:confirmPassword"));
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 });
const isPassword = config?.isPassword;
const isReadOnly = config?.isReadOnly;
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20, readOnly: isReadOnly });
input.setOrigin(0, 0);
inputContainer.add(inputBg);
inputContainer.add(input);
this.modalContainer.add(inputContainer);
this.inputContainers.push(inputContainer);
this.modalContainer.add(inputContainer);
this.inputs.push(input);
});
this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT);
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
this.errorMessage.setVisible(false);
this.modalContainer.add(this.errorMessage);
}
show(args: any[]): boolean {
@ -149,3 +168,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
}
}
}
export interface InputFieldConfig {
label: string,
isPassword?: boolean,
isReadOnly?: boolean
}

View File

@ -1,4 +1,4 @@
import { FormModalUiHandler } from "./form-modal-ui-handler";
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
import { ModalConfig } from "./modal-ui-handler";
import * as Utils from "../utils";
import { Mode } from "./ui";
@ -17,9 +17,9 @@ interface BuildInteractableImageOpts {
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_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";
@ -75,10 +75,6 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
return i18next.t("menu:login");
}
override getFields(_config?: ModalConfig): string[] {
return [ i18next.t("menu:username"), i18next.t("menu:password") ];
}
override getWidth(_config?: ModalConfig): number {
return 160;
}
@ -106,14 +102,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
case this.ERR_PASSWORD_MATCH:
return i18next.t("menu:unmatchingPassword");
case this.ERR_NO_SAVES:
return i18next.t("menu:noSaves");
return "P01: " + i18next.t("menu:noSaves");
case this.ERR_TOO_MANY_SAVES:
return i18next.t("menu:tooManySaves");
return "P02: " + i18next.t("menu:tooManySaves");
}
return super.getReadableErrorMessage(error);
}
override getInputFieldConfigs(): InputFieldConfig[] {
const inputFieldConfigs: InputFieldConfig[] = [];
inputFieldConfigs.push({ label: i18next.t("menu:username") });
inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true });
return inputFieldConfigs;
}
override show(args: any[]): boolean {
if (super.show(args)) {
@ -164,7 +167,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
[ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown"));
}
private processExternalProvider(config: ModalConfig) : void {
private processExternalProvider(config: ModalConfig): void {
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length);
this.externalPartyTitle.setVisible(true);

View File

@ -13,6 +13,7 @@ import { GameDataType } from "#enums/game-data-type";
import BgmBar from "#app/ui/bgm-bar";
import AwaitableUiHandler from "./awaitable-ui-handler";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { AdminMode, getAdminModeName } from "./admin-ui-handler";
enum MenuOptions {
GAME_SETTINGS,
@ -387,16 +388,41 @@ export default class MenuUiHandler extends MessageUiHandler {
communityOptions.push({
label: "Admin",
handler: () => {
ui.playSelect();
ui.setOverlayMode(Mode.ADMIN, {
buttonActions: [
() => {
ui.revertMode();
},
() => {
ui.revertMode();
const skippedAdminModes: AdminMode[] = [ AdminMode.ADMIN ]; // this is here so that we can skip the menu populating enums that aren't meant for the menu, such as the AdminMode.ADMIN
const options: OptionSelectItem[] = [];
Object.values(AdminMode).filter((v) => !isNaN(Number(v)) && !skippedAdminModes.includes(v as AdminMode)).forEach((mode) => { // this gets all the enums in a way we can use
options.push({
label: getAdminModeName(mode as AdminMode),
handler: () => {
ui.playSelect();
ui.setOverlayMode(Mode.ADMIN, {
buttonActions: [
// we double revert here and below to go back 2 layers of menus
() => {
ui.revertMode();
ui.revertMode();
},
() => {
ui.revertMode();
ui.revertMode();
}
]
}, mode); // mode is our AdminMode enum
return true;
}
]
});
});
options.push({
label: "Cancel",
handler: () => {
ui.revertMode();
return true;
}
});
this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
options: options,
delay: 0
});
return true;
},

View File

@ -15,12 +15,14 @@ export abstract class ModalUiHandler extends UiHandler {
protected titleText: Phaser.GameObjects.Text;
protected buttonContainers: Phaser.GameObjects.Container[];
protected buttonBgs: Phaser.GameObjects.NineSlice[];
protected buttonLabels: Phaser.GameObjects.Text[];
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
this.buttonContainers = [];
this.buttonBgs = [];
this.buttonLabels = [];
}
abstract getModalTitle(config?: ModalConfig): string;
@ -75,6 +77,7 @@ export abstract class ModalUiHandler extends UiHandler {
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
this.buttonLabels.push(buttonLabel);
this.buttonBgs.push(buttonBg);
this.buttonContainers.push(buttonContainer);

View File

@ -279,11 +279,8 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
this.pokemonAbilityText.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme));
this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, this.scene.uiTheme));
const ownedAbilityAttrs = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr;
// Check if the player owns ability for the root form
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs);
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(starterEntry.abilityAttr);
if (!playerOwnsThisAbility) {
this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme));

View File

@ -1,4 +1,4 @@
import { FormModalUiHandler } from "./form-modal-ui-handler";
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
import { ModalConfig } from "./modal-ui-handler";
import * as Utils from "../utils";
import { Mode } from "./ui";
@ -24,10 +24,6 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
return i18next.t("menu:register");
}
getFields(config?: ModalConfig): string[] {
return [ i18next.t("menu:username"), i18next.t("menu:password"), i18next.t("menu:confirmPassword") ];
}
getWidth(config?: ModalConfig): number {
return 160;
}
@ -61,6 +57,14 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
return super.getReadableErrorMessage(error);
}
override getInputFieldConfigs(): InputFieldConfig[] {
const inputFieldConfigs: InputFieldConfig[] = [];
inputFieldConfigs.push({ label: i18next.t("menu:username") });
inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true });
inputFieldConfigs.push({ label: i18next.t("menu:confirmPassword"), isPassword: true });
return inputFieldConfigs;
}
setup(): void {
super.setup();

View File

@ -1,4 +1,4 @@
import { FormModalUiHandler } from "./form-modal-ui-handler";
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
import { ModalConfig } from "./modal-ui-handler";
import i18next from "i18next";
import { PlayerPokemon } from "#app/field/pokemon";
@ -8,10 +8,6 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
return i18next.t("menu:renamePokemon");
}
getFields(config?: ModalConfig): string[] {
return [ i18next.t("menu:nickname") ];
}
getWidth(config?: ModalConfig): number {
return 160;
}
@ -33,6 +29,10 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
return super.getReadableErrorMessage(error);
}
override getInputFieldConfigs(): InputFieldConfig[] {
return [{ label: i18next.t("menu:nickname") }];
}
show(args: any[]): boolean {
if (super.show(args)) {
const config = args[0] as ModalConfig;

View File

@ -1,4 +1,4 @@
import { FormModalUiHandler } from "./form-modal-ui-handler";
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
import { ModalConfig } from "./modal-ui-handler";
import i18next from "i18next";
import { PlayerPokemon } from "#app/field/pokemon";
@ -43,10 +43,6 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
return "Test Dialogue";
}
getFields(config?: ModalConfig): string[] {
return [ "Dialogue" ];
}
getWidth(config?: ModalConfig): number {
return 300;
}
@ -68,8 +64,15 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
return super.getReadableErrorMessage(error);
}
override getInputFieldConfigs(): InputFieldConfig[] {
return [{ label: "Dialogue" }];
}
show(args: any[]): boolean {
const ui = this.getUi();
const hasTitle = !!this.getModalTitle();
this.updateFields(this.getInputFieldConfigs(), hasTitle);
this.updateContainer(args[0] as ModalConfig);
const input = this.inputs[0];
input.setMaxLength(255);