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: case Species.ZYGARDE:
return Utils.randSeedInt(4); return Utils.randSeedInt(4);
case Species.MINIOR: case Species.MINIOR:
return Utils.randSeedInt(6); return Utils.randSeedInt(7);
case Species.ALCREMIE: case Species.ALCREMIE:
return Utils.randSeedInt(9); return Utils.randSeedInt(9);
case Species.MEOWSTIC: case Species.MEOWSTIC:

View File

@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag } from "./battler-tags"; import { BattlerTag, GroundedTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender"; 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 { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain"; 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 { apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
// Disable showAbility during getTargetBenefitScore // Disable showAbility during getTargetBenefitScore
this.showAbility = args[4]; 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; return false;
} }
@ -1329,7 +1331,6 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
*/ */
const exceptAttrs: Constructor<MoveAttr>[] = [ const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr, MultiHitAttr,
ChargeAttr,
SacrificialAttr, SacrificialAttr,
SacrificialAttrOnHit 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 */ /** Also check if this move is an Attack move and if it's only targeting one Pokemon */
return numTargets === 1 return numTargets === 1
&& !move.isChargingMove()
&& !exceptAttrs.some(attr => move.hasAttr(attr)) && !exceptAttrs.some(attr => move.hasAttr(attr))
&& !exceptMoves.some(id => move.id === id) && !exceptMoves.some(id => move.id === id)
&& move.category !== MoveCategory.STATUS; && move.category !== MoveCategory.STATUS;
@ -2420,11 +2422,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
super(true); 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(); const targets = pokemon.getOpponents();
if (simulated || !targets.length) { if (simulated || !targets.length) {
return simulated; return simulated;
} }
const promises: Promise<void>[] = [];
let target: Pokemon; let target: Pokemon;
if (targets.length > 1) { if (targets.length > 1) {
@ -2433,7 +2436,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
target = targets[0]; target = targets[0];
} }
target = target!; // compiler doesn't know its guranteed to be defined target = target!;
pokemon.summonData.speciesForm = target.getSpeciesForm(); pokemon.summonData.speciesForm = target.getSpeciesForm();
pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.ability = target.getAbility().id;
@ -2450,18 +2453,26 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
pokemon.setStatStage(s, target.getStatStage(s)); 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.moveset = target.getMoveset().map((m) => {
pokemon.summonData.types = target.getTypes(); 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));
pokemon.scene.playSound("battle_anims/PRSFX- Transform"); } else {
console.warn(`Imposter: somehow iterating over a ${m} value when copying moveset!`);
pokemon.loadAssets(false).then(() => { return new PokemonMove(Moves.NONE);
pokemon.playAnim(); }
pokemon.updateInfo();
}); });
pokemon.summonData.types = target.getTypes();
promises.push(pokemon.updateInfo());
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, })); 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; return true;
} }
@ -4200,6 +4211,11 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr {
export class BlockRedirectAbAttr extends AbAttr { } 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 { export class ReduceStatusEffectDurationAbAttr extends AbAttr {
private statusEffect: StatusEffect; private statusEffect: StatusEffect;
@ -4209,9 +4225,19 @@ export class ReduceStatusEffectDurationAbAttr extends AbAttr {
this.statusEffect = statusEffect; 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) { 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; 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) */ /** Attribute for abilities that allow moves that make contact to ignore protection (i.e. Unseen Fist) */
export class IgnoreProtectOnContactAbAttr extends AbAttr { } 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 { export class UncopiableAbilityAbAttr extends AbAttr {
constructor() { constructor() {
super(false); super(false);
@ -5321,7 +5371,8 @@ export function initAbilities() {
.attr(PostSummonTransformAbAttr) .attr(PostSummonTransformAbAttr)
.attr(UncopiableAbilityAbAttr), .attr(UncopiableAbilityAbAttr),
new Ability(Abilities.INFILTRATOR, 5) new Ability(Abilities.INFILTRATOR, 5)
.unimplemented(), .attr(InfiltratorAbAttr)
.partial(), // does not bypass Mist
new Ability(Abilities.MUMMY, 5) new Ability(Abilities.MUMMY, 5)
.attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY) .attr(PostDefendAbilityGiveAbAttr, Abilities.MUMMY)
.bypassFaint(), .bypassFaint(),

View File

@ -7,7 +7,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { BattlerIndex } from "#app/battle"; 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 { Stat } from "#enums/stat";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims";
import i18next from "i18next"; import i18next from "i18next";
@ -130,7 +130,18 @@ export class MistTag extends ArenaTag {
* to flag the stat reduction as cancelled * to flag the stat reduction as cancelled
* @returns `true` if a stat reduction was cancelled; `false` otherwise * @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; cancelled.value = true;
if (!simulated) { if (!simulated) {
@ -169,12 +180,18 @@ export class WeakenMoveScreenTag extends ArenaTag {
* *
* @param arena the {@linkcode Arena} where the move is applied. * @param arena the {@linkcode Arena} where the move is applied.
* @param simulated n/a * @param simulated n/a
* @param attacker the attacking {@linkcode Pokemon}
* @param moveCategory the attacking move's {@linkcode MoveCategory}. * @param moveCategory the attacking move's {@linkcode MoveCategory}.
* @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier * @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier
* @returns `true` if the attacking move was weakened; `false` otherwise. * @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)) { 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; damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5;
return true; return true;
} }
@ -953,6 +970,9 @@ export class GravityTag extends ArenaTag {
if (pokemon !== null) { if (pokemon !== null) {
pokemon.removeTag(BattlerTagType.FLOATING); pokemon.removeTag(BattlerTagType.FLOATING);
pokemon.removeTag(BattlerTagType.TELEKINESIS); 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 { Gender } from "#app/data/gender";
import { PokeballType } from "#app/data/pokeball"; import { PokeballType } from "#app/data/pokeball";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import { Stat } from "#enums/stat";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
@ -10,7 +9,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day"; 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"; 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)) new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY))
], ],
[Species.TYROGUE]: [ [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])), * Custom: Evolves into Hitmonlee, Hitmonchan or Hitmontop at level 20
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF])) * 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]: [ [Species.KOFFING]: [
new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), 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 new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier + 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 new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier + 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 { battleAnimRawData } from "./battle-anim-raw-data";
import BattleScene from "../battle-scene"; 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 Pokemon from "../field/pokemon";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
@ -476,8 +476,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else { } else {
const loadedCheckTimer = setInterval(() => { const loadedCheckTimer = setInterval(() => {
if (moveAnims.get(move) !== null) { if (moveAnims.get(move) !== null) {
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0]; const chargeAnimSource = (allMoves[move].isChargingMove())
if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) { ? allMoves[move]
: (allMoves[move].getAttrs(DelayedAttackAttr)[0]
?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) {
return; return;
} }
clearInterval(loadedCheckTimer); clearInterval(loadedCheckTimer);
@ -507,11 +510,12 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
} else { } else {
populateMoveAnim(move, ba); populateMoveAnim(move, ba);
} }
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] const chargeAnimSource = (allMoves[move].isChargingMove())
|| allMoves[move].getAttrs(DelayedAttackAttr)[0] ? allMoves[move]
|| allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]; : (allMoves[move].getAttrs(DelayedAttackAttr)[0]
if (chargeAttr) { ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve()); if (chargeAnimSource) {
initMoveChargeAnim(scene, chargeAnimSource.chargeAnim).then(() => resolve());
} else { } else {
resolve(); resolve();
} }
@ -638,11 +642,12 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo
return new Promise(resolve => { return new Promise(resolve => {
const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat();
for (const moveId of moveIds) { for (const moveId of moveIds) {
const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0] const chargeAnimSource = (allMoves[moveId].isChargingMove())
|| allMoves[moveId].getAttrs(DelayedAttackAttr)[0] ? allMoves[moveId]
|| allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]; : (allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
if (chargeAttr) { ?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]);
const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim); if (chargeAnimSource) {
const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim);
moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct? moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct?
if (Array.isArray(moveChargeAnims)) { if (Array.isArray(moveChargeAnims)) {
moveAnimations.push(moveChargeAnims[1]); moveAnimations.push(moveChargeAnims[1]);

View File

@ -1,29 +1,43 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; import BattleScene from "#app/battle-scene";
import { getPokemonNameWithAffix } from "../messages"; import {
import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; allAbilities,
import { StatusEffect } from "./status-effect"; applyAbAttrs,
import * as Utils from "../utils"; BlockNonDirectDamageAbAttr,
import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move"; FlinchEffectAbAttr,
import { Type } from "./type"; ProtectStatAbAttr,
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; ReverseDrainAbAttr
import { TerrainType } from "./terrain"; } from "#app/data/ability";
import { WeatherType } from "./weather"; import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims";
import { allAbilities } from "./ability"; import Move, {
import { SpeciesFormChangeManualTrigger } from "./pokemon-forms"; allMoves,
import { Abilities } from "#enums/abilities"; applyMoveAttrs,
import { BattlerTagType } from "#enums/battler-tag-type"; ConsecutiveUseDoublePowerAttr,
import { Moves } from "#enums/moves"; HealOnAllyAttr,
import { Species } from "#enums/species"; MoveCategory,
import i18next from "#app/plugins/i18n"; MoveFlags,
import { Stat, type BattleStat, type EffectiveStat, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; 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 { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; import { StatStageChangeCallback, StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import i18next from "#app/plugins/i18n";
import BattleScene from "#app/battle-scene"; 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 { export enum BattlerTagLapseType {
FAINT, FAINT,
@ -33,6 +47,7 @@ export enum BattlerTagLapseType {
MOVE_EFFECT, MOVE_EFFECT,
TURN_END, TURN_END,
HIT, HIT,
AFTER_HIT,
CUSTOM CUSTOM
} }
@ -405,7 +420,7 @@ export class RechargingTag extends BattlerTag {
*/ */
export class BeakBlastChargingTag extends BattlerTag { export class BeakBlastChargingTag extends BattlerTag {
constructor() { 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 { 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) * 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 pokemon {@linkcode Pokemon} the owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle * @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 { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.AFTER_HIT) {
const effectPhase = pokemon.scene.getCurrentPhase(); const phaseData = getMoveEffectPhaseData(pokemon);
if (effectPhase instanceof MoveEffectPhase) { if (phaseData?.move.hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon(); phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
}
} }
return true; return true;
} }
@ -444,11 +456,10 @@ export class BeakBlastChargingTag extends BattlerTag {
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap} * @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap}
*/ */
export class ShellTrapTag extends BattlerTag { export class ShellTrapTag extends BattlerTag {
public activated: boolean; public activated: boolean = false;
constructor() { constructor() {
super(BattlerTagType.SHELL_TRAP, BattlerTagLapseType.TURN_END, 1); super(BattlerTagType.SHELL_TRAP, [ BattlerTagLapseType.TURN_END, BattlerTagLapseType.AFTER_HIT ], 1);
this.activated = false;
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -459,25 +470,33 @@ export class ShellTrapTag extends BattlerTag {
* "Activates" the shell trap, causing the tag owner to move next. * "Activates" the shell trap, causing the tag owner to move next.
* @param pokemon {@linkcode Pokemon} the owner of this tag * @param pokemon {@linkcode Pokemon} the owner of this tag
* @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle * @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 { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.AFTER_HIT) {
const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex( const phaseData = getMoveEffectPhaseData(pokemon);
phase => phase instanceof MovePhase && phase.pokemon === pokemon
);
const firstMovePhaseIndex = pokemon.scene.phaseQueue.findIndex(
phase => phase instanceof MovePhase
);
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { // Trap should only be triggered by opponent's Physical moves
const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase); 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 true;
} }
return super.lapse(pokemon, lapseType); return super.lapse(pokemon, lapseType);
} }
} }
@ -641,7 +660,7 @@ export class ConfusedTag extends BattlerTag {
if (pokemon.randSeedInt(3) === 0) { if (pokemon.randSeedInt(3) === 0) {
const atk = pokemon.getEffectiveStat(Stat.ATK); const atk = pokemon.getEffectiveStat(Stat.ATK);
const def = pokemon.getEffectiveStat(Stat.DEF); const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = Utils.toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.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.scene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage); pokemon.damageAndUpdate(damage);
pokemon.battleData.hitCount++; pokemon.battleData.hitCount++;
@ -812,13 +831,13 @@ export class SeedTag extends BattlerTag {
if (ret) { if (ret) {
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (source) { if (source) {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED)); 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); const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(), pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(),
!reverseDrain ? damage : damage * -1, !reverseDrain ? damage : damage * -1,
@ -838,7 +857,7 @@ export class SeedTag extends BattlerTag {
export class NightmareTag extends BattlerTag { export class NightmareTag extends BattlerTag {
constructor() { constructor() {
super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.AFTER_MOVE, 1, Moves.NIGHTMARE); super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, Moves.NIGHTMARE);
} }
onAdd(pokemon: Pokemon): void { 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.queueMessage(i18next.t("battlerTags:nightmareLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type 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); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { 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; return false;
} }
if (allMoves[repeatableMove.move].hasAttr(ChargeAttr) && repeatableMove.result === MoveResult.OTHER) {
return false;
}
this.moveId = repeatableMove.move; this.moveId = repeatableMove.move;
return true; return true;
@ -1004,7 +1019,7 @@ export class IngrainTag extends TrappedTag {
new PokemonHealPhase( new PokemonHealPhase(
pokemon.scene, pokemon.scene,
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16), toDmgValue(pokemon.getMaxHp() / 16),
i18next.t("battlerTags:ingrainLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), i18next.t("battlerTags:ingrainLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }),
true true
) )
@ -1067,7 +1082,7 @@ export class AquaRingTag extends BattlerTag {
new PokemonHealPhase( new PokemonHealPhase(
pokemon.scene, pokemon.scene,
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / 16), toDmgValue(pokemon.getMaxHp() / 16),
i18next.t("battlerTags:aquaRingLapse", { i18next.t("battlerTags:aquaRingLapse", {
moveName: this.getMoveName(), moveName: this.getMoveName(),
pokemonName: getPokemonNameWithAffix(pokemon) 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)); 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); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { 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)) { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
const attacker = effectPhase.getPokemon(); const attacker = effectPhase.getPokemon();
if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { 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 { onRemove(pokemon: Pokemon): void {
// Wait 2 frames before setting visible for battle animations that don't immediately show the sprite invisible // Wait 2 frames before setting visible for battle animations that don't immediately show the sprite invisible
pokemon.scene.tweens.addCounter({ pokemon.scene.tweens.addCounter({
duration: Utils.getFrameMs(2), duration: getFrameMs(2),
onComplete: () => pokemon.setVisible(true) onComplete: () => pokemon.setVisible(true)
}); });
} }
@ -1860,12 +1875,12 @@ export class SaltCuredTag extends BattlerTag {
if (ret) { if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE)); 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); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
const pokemonSteelOrWater = pokemon.isOfType(Type.STEEL) || pokemon.isOfType(Type.WATER); 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( pokemon.scene.queueMessage(
i18next.t("battlerTags:saltCuredLapse", { i18next.t("battlerTags:saltCuredLapse", {
@ -1907,11 +1922,11 @@ export class CursedTag extends BattlerTag {
if (ret) { if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.SALT_CURE)); 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); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (!cancelled.value) { 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) })); pokemon.scene.queueMessage(i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
} }
} }
@ -2173,7 +2188,7 @@ export class GulpMissileTag extends BattlerTag {
return true; return true;
} }
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
if (!cancelled.value) { 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 * @returns `true` if the move cannot be used because the target is an ally
*/ */
override isMoveTargetRestricted(move: Moves, user: Pokemon, target: Pokemon) { 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); applyMoveAttrs(StatusCategoryOnAllyAttr, user, target, allMoves[move], moveCategory);
if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) { if (allMoves[move].hasAttr(HealOnAllyAttr) && moveCategory.value === MoveCategory.STATUS ) {
return true; return true;
@ -2506,7 +2521,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
const ret = super.lapse(pokemon, lapseType); const ret = super.lapse(pokemon, lapseType);
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.CUSTOM) {
const cancelled = new Utils.BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
if (!cancelled.value) { if (!cancelled.value) {
if (pokemon.mysteryEncounterBattleEffects) { 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 // 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 // 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 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); const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS);
if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) {
return true; return true;
@ -2955,3 +2970,22 @@ export function loadBattlerTag(source: BattlerTag | any): BattlerTag {
tag.loadTag(source); tag.loadTag(source);
return tag; 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 * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; 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 { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; import { BattlerIndex, BattleType } from "../battle";
import { TerrainType } from "./terrain"; 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 * @returns boolean
*/ */
isAllyTarget(): boolean { isAllyTarget(): boolean {
switch (this.moveTarget) { switch (this.moveTarget) {
case MoveTarget.USER: case MoveTarget.USER:
@ -305,6 +304,10 @@ export default class Move implements Localizable {
return false; return false;
} }
isChargingMove(): this is ChargingMove {
return false;
}
/** /**
* Checks if the move is immune to certain types. * 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. * 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 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.SOUND_BASED)
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE); && !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 * Base class defining all {@linkcode Move} Attributes
* @abstract * @abstract
@ -2035,15 +2121,15 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
export class StatusEffectAttr extends MoveEffectAttr { export class StatusEffectAttr extends MoveEffectAttr {
public effect: StatusEffect; public effect: StatusEffect;
public cureTurn: integer | null; public turnsRemaining?: number;
public overrideStatus: boolean; 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); super(selfTarget, MoveEffectTrigger.HIT);
this.effect = effect; this.effect = effect;
this.cureTurn = cureTurn!; // TODO: is this bang correct? this.turnsRemaining = turnsRemaining;
this.overrideStatus = !!overrideStatus; this.overrideStatus = overrideStatus;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { 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) { if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
} }
return false; return false;
} }
if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) 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); applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true; return true;
} }
@ -2087,8 +2173,8 @@ export class StatusEffectAttr extends MoveEffectAttr {
export class MultiStatusEffectAttr extends StatusEffectAttr { export class MultiStatusEffectAttr extends StatusEffectAttr {
public effects: StatusEffect[]; public effects: StatusEffect[];
constructor(effects: StatusEffect[], selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) { constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
super(effects[0], selfTarget, cureTurn, overrideStatus); super(effects[0], selfTarget, turnsRemaining, overrideStatus);
this.effects = effects; 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 { export class OverrideMoveEffectAttr extends MoveAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
//const overridden = args[0] as Utils.BooleanHolder; //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 { export class DelayedAttackAttr extends OverrideMoveEffectAttr {
public tagType: ArenaTagType; public tagType: ArenaTagType;
public chargeAnim: ChargeAnim; public chargeAnim: ChargeAnim;
@ -4864,6 +4901,37 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
return true; 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 { export class AddBattlerTagAttr extends MoveEffectAttr {
public tagType: BattlerTagType; public tagType: BattlerTagType;
public turnCountMin: integer; public turnCountMin: integer;
@ -5151,7 +5219,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { 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) { if (move.category === MoveCategory.STATUS) {
user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) })); user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
} }
@ -6566,7 +6634,7 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
return false; return false;
} }
if (allMoves[copiableMove.move].hasAttr(ChargeAttr) && copiableMove.result === MoveResult.OTHER) { if (allMoves[copiableMove.move].isChargingMove() && copiableMove.result === MoveResult.OTHER) {
return false; return false;
} }
@ -6858,42 +6926,52 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
} }
export class TransformAttr extends MoveEffectAttr { export class TransformAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => { if (!super.apply(user, target, move, args)) {
if (!super.apply(user, target, move, args)) { return false;
return resolve(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> { 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); 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); 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 { export class MoveCondition {
protected func: MoveConditionFunc; protected func: MoveConditionFunc;
@ -7520,8 +7616,8 @@ export function initMoves() {
new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr) .attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr), .attr(OneHitKOAccuracyAttr),
new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) new ChargingAttackMove(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}" })) .chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" }))
.attr(HighCritAttr) .attr(HighCritAttr)
.windMove() .windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -7540,8 +7636,9 @@ export function initMoves() {
.hidesTarget() .hidesTarget()
.windMove() .windMove()
.partial(), // Should force random switches .partial(), // Should force random switches
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) new ChargingAttackMove(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) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
.attr(TrapAttr, BattlerTagType.BIND), .attr(TrapAttr, BattlerTagType.BIND),
@ -7689,8 +7786,9 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.slicingMove() .slicingMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) new ChargingAttackMove(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}" })) .chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
.attr(AntiSunlightPowerDecreaseAttr), .attr(AntiSunlightPowerDecreaseAttr),
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
@ -7738,8 +7836,9 @@ export function initMoves() {
.attr(OneHitKOAccuracyAttr) .attr(OneHitKOAccuracyAttr)
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) new ChargingAttackMove(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), .chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND),
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(StatusEffectAttr, StatusEffect.TOXIC)
.attr(ToxicAccuracyAttr), .attr(ToxicAccuracyAttr),
@ -7830,9 +7929,9 @@ export function initMoves() {
.attr(TrapAttr, BattlerTagType.CLAMP), .attr(TrapAttr, BattlerTagType.CLAMP),
new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1) new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) new ChargingAttackMove(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) .chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1) new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
@ -7868,13 +7967,14 @@ export function initMoves() {
.triageMove(), .triageMove(),
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP), .attr(StatusEffectAttr, StatusEffect.SLEEP),
new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) new ChargingAttackMove(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}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(FlinchAttr) .attr(FlinchAttr)
.makesContact(false), .makesContact(false),
new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1) new StatusMove(Moves.TRANSFORM, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(TransformAttr) .attr(TransformAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.ignoresProtect(), .ignoresProtect(),
new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.BUBBLE, Type.WATER, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
@ -8298,7 +8398,7 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .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) 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(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false), .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) new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3)
.makesContact(false) .makesContact(false)
.attr(SecretPowerAttr), .attr(SecretPowerAttr),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) new ChargingAttackMove(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) .chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }))
.attr(GulpMissileTagAttr), .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER)
.chargeAttr(GulpMissileTagAttr),
new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3)
.attr(MultiHitAttr), .attr(MultiHitAttr),
new SelfStatusMove(Moves.CAMOUFLAGE, Type.NORMAL, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.CAMOUFLAGE, Type.NORMAL, -1, 20, -1, 0, 3)
@ -8459,8 +8560,9 @@ export function initMoves() {
.attr(RechargeAttr), .attr(RechargeAttr),
new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true),
new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) new ChargingAttackMove(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) .chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new AttackMove(Moves.MUD_SHOT, Type.GROUND, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 3) 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) 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) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.windMove(), .windMove(),
new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) new ChargingAttackMove(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) .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect(), .ignoresProtect(),
new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5) new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.ACC ], 1, true),
@ -8935,11 +9038,12 @@ export function initMoves() {
.attr( .attr(
MovePowerMultiplierAttr, MovePowerMultiplierAttr,
(user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1), (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) new ChargingAttackMove(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/ .chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }))
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }), BattlerTagType.FLYING) // TODO: Add 2nd turn message .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition) .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) new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, 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) new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.danceMove(), .danceMove(),
new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) new ChargingAttackMove(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}" })) .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" }))
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) new ChargingAttackMove(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}" })) .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" }))
.attr(StatusEffectAttr, StatusEffect.BURN), .attr(StatusEffectAttr, StatusEffect.BURN),
new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
@ -9135,8 +9239,9 @@ export function initMoves() {
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE),
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new ChargingAttackMove(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) .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect(), .ignoresProtect(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GHOST) .attr(AddTypeAttr, Type.GHOST)
@ -9245,8 +9350,8 @@ export function initMoves() {
.ignoresSubstitute() .ignoresSubstitute()
.powderMove() .powderMove()
.unimplemented(), .unimplemented(),
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) 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))) .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) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
.triageMove(), .triageMove(),
new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) new ChargingAttackMove(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}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
.attr(AntiSunlightPowerDecreaseAttr) .attr(AntiSunlightPowerDecreaseAttr)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) 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(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8) new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }), null, true) .chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true), .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true),
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr) .attr(ShellSideArmCategoryAttr)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
@ -10261,8 +10367,10 @@ export function initMoves() {
.attr(IvyCudgelTypeAttr) .attr(IvyCudgelTypeAttr)
.attr(HighCritAttr) .attr(HighCritAttr)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
.attr(ElectroShotChargeAttr), .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) new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
.attr(TeraMoveCategoryAttr) .attr(TeraMoveCategoryAttr)
.attr(TeraStarstormTypeAttr) .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 { modifierTypes } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; 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 { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import i18next from "i18next";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/anOfferYouCantRefuse"; 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()); encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName());
return true; return true;
@ -123,7 +126,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
return true; return true;
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Give the player a Shiny charm // Give the player a Shiny Charm
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM));
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
@ -132,9 +135,11 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
.withOption( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(
new MoveRequirement(EXTORTION_MOVES, true), CombinationPokemonRequirement.Some(
new AbilityRequirement(EXTORTION_ABILITIES, true)) new MoveRequirement(EXTORTION_MOVES, true),
new AbilityRequirement(EXTORTION_ABILITIES, true)
)
) )
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, 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 = export const BugTypeSuperfanEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN)
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team CombinationPokemonRequirement.Some(
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
new TypeRequirement(Type.BUG, false, 1) new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1),
)) new TypeRequirement(Type.BUG, false, 1)
)
)
.withMaxAllowedEncounters(1) .withMaxAllowedEncounters(1)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([]) // These are set in onInit() .withIntroSpriteConfigs([]) // These are set in onInit()
@ -405,11 +407,13 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
.build()) .build())
.withOption(MysteryEncounterOptionBuilder .withOption(MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( .withPrimaryPokemonRequirement(
// Meets one or both of the below reqs CombinationPokemonRequirement.Some(
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), // Meets one or both of the below reqs
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
)) new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1)
)
)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,

View File

@ -11,7 +11,7 @@ import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Abilities } from "#enums/abilities"; 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 { Type } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -425,17 +425,8 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Do ability swap // Do ability swap
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
if (pokemon.isFusion()) {
if (!pokemon.fusionCustomPokemonData) { applyAbilityOverrideToPokemon(pokemon, encounter.misc.ability);
pokemon.fusionCustomPokemonData = new CustomPokemonData();
}
pokemon.fusionCustomPokemonData.ability = encounter.misc.ability;
} else {
if (!pokemon.customPokemonData) {
pokemon.customPokemonData = new CustomPokemonData();
}
pokemon.customPokemonData.ability = encounter.misc.ability;
}
encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); 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 { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/darkDeal"; 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 // 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 // Will never return last battle able mon and instead pick fainted/unable to battle
const removedPokemon = getRandomPlayerPokemon(scene, true, false, true); const removedPokemon = getRandomPlayerPokemon(scene, true, false, true);
// Get all the pokemon's held items // Get all the pokemon's held items
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removedPokemon); scene.removePokemonFromPlayerParty(removedPokemon);
@ -160,7 +162,13 @@ export const DarkDealEncounter: MysteryEncounter =
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL));
// Start encounter with random legendary (7-10 starter strength) that has level additive // 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; const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers;
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
const roll = randSeedInt(100); const roll = randSeedInt(100);
@ -172,7 +180,8 @@ export const DarkDealEncounter: MysteryEncounter =
isBoss: true, isBoss: true,
modifierConfigs: bossModifiers.map(m => { modifierConfigs: bossModifiers.map(m => {
return { return {
modifier: m modifier: m,
stackCount: m.getStackCount(),
}; };
}) })
}; };

View File

@ -45,10 +45,13 @@ export const DelibirdyEncounter: MysteryEncounter =
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .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 .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 .withPrimaryPokemonRequirement(
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), CombinationPokemonRequirement.Some(
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) // 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([ .withIntroSpriteConfigs([
{ {
spriteKey: "", spriteKey: "",
@ -196,7 +199,7 @@ export const DelibirdyEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; 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) { if (modifier instanceof BerryModifier) {
// Check if the player has max stacks of that Candy Jar already // Check if the player has max stacks of that Candy Jar already
const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; 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)); scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR));
} }
} else { } else {
// Check if the player has max stacks of that Healing Charm already // Check if the player has max stacks of that Berry Pouch already
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
@ -221,7 +224,7 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } 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 encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const modifier = encounter.misc.chosenModifier;
// Check if the player has max stacks of Berry Pouch already // Check if the player has max stacks of Healing Charm already
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
@ -300,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter =
scene.playSound("item_fanfare"); scene.playSound("item_fanfare");
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
} else { } 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 // 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 { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; 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 { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon"; import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
import { WeatherType } from "#app/data/weather"; import { WeatherType } from "#app/data/weather";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; 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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; 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 */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieryFallout"; const namespace = "mysteryEncounters/fieryFallout";
@ -62,16 +68,24 @@ export const FieryFalloutEncounter: MysteryEncounter =
{ {
species: volcaronaSpecies, species: volcaronaSpecies,
isBoss: false, 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, species: volcaronaSpecies,
isBoss: false, 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, doubleBattle: true,
disableSwitch: true disableSwitch: true,
}; };
encounter.enemyPartyConfigs = [ config ]; encounter.enemyPartyConfigs = [ config ];
@ -139,7 +153,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Pick battle // Pick battle
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonAttackTypeBoostItem(scene));
encounter.startOfBattleEffects.push( encounter.startOfBattleEffects.push(
{ {
@ -153,18 +167,6 @@ export const FieryFalloutEncounter: MysteryEncounter =
targets: [ BattlerIndex.PLAYER_2 ], targets: [ BattlerIndex.PLAYER_2 ],
move: new PokemonMove(Moves.FIRE_SPIN), move: new PokemonMove(Moves.FIRE_SPIN),
ignorePp: true 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]); await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
} }
@ -180,7 +182,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
], ],
}, },
async (scene: BattleScene) => { 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 encounter = scene.currentBattle.mysteryEncounter!;
const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); 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)) { if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
// Burn applied // Burn applied
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
encounter.setDialogueToken("abilityName", new Ability(Abilities.HEATPROOF, 3).name);
queueEncounterMessage(scene, `${namespace}:option.2.target_burned`); 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( .withOption(
MysteryEncounterOptionBuilder MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically .withPrimaryPokemonRequirement(
.withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically CombinationPokemonRequirement.Some(
new TypeRequirement(Type.FIRE, true, 1),
new AbilityRequirement(FIRE_RESISTANT_ABILITIES, true)
)
) // Will set option3PrimaryName dialogue token automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,
@ -233,26 +243,32 @@ export const FieryFalloutEncounter: MysteryEncounter =
{ fillRemaining: true }, { fillRemaining: true },
undefined, undefined,
() => { () => {
giveLeadPokemonCharcoal(scene); giveLeadPokemonAttackTypeBoostItem(scene);
}); });
const primary = encounter.options[2].primaryPokemon!; 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); leaveEncounterWithoutBattle(scene);
}) })
.build() .build()
) )
.build(); .build();
function giveLeadPokemonCharcoal(scene: BattleScene) { function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) {
// Give first party pokemon Charcoal for free at end of battle // Give first party pokemon attack type boost item for free at end of battle
const leadPokemon = scene.getParty()?.[0]; const leadPokemon = scene.getParty()?.[0];
if (leadPokemon) { if (leadPokemon) {
const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType; // Generate type booster held item, default to Charcoal if item fails to generate
applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType;
scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); if (!boosterModifierType) {
queueEncounterMessage(scene, `${namespace}:found_charcoal`); 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 // 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 // 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( const hardTemplate = new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true),
new TrainerPartyTemplate( new TrainerPartyTemplate(

View File

@ -21,7 +21,7 @@ import i18next from "i18next";
const namespace = "mysteryEncounters/shadyVitaminDealer"; const namespace = "mysteryEncounters/shadyVitaminDealer";
const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; 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. * Shady Vitamin Dealer encounter.

View File

@ -222,7 +222,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon1; encounter.misc.chosenPokemon = pokemon1;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); 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 // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
@ -271,7 +274,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon2; encounter.misc.chosenPokemon = pokemon2;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); 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 // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
@ -320,7 +326,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
encounter.misc.chosenPokemon = pokemon3; encounter.misc.chosenPokemon = pokemon3;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); 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 // Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3); removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
@ -454,12 +463,16 @@ function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number]
} }
// Maximum of 30 points // 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 // First 5 points go to Common eggs
const numRares = Math.floor(totalPoints / 6); 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 // 1 Common egg for every point leftover
const numCommons = totalPoints % 6; numCommons += totalPoints % 4;
return [ numCommons, numRares ]; 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 .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party
.withFleeAllowed(false) .withFleeAllowed(false)
.withHideWildIntroMessage(true) .withHideWildIntroMessage(true)
.withPreventGameStatsUpdates(true) // Do not count the Pokemon as seen or defeated since it is ours
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "training_session_gear", 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 ] moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ]
}; };
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
levelAdditiveModifier: 1, levelAdditiveModifier: 0.5,
pokemonConfigs: [ pokemonConfig ], pokemonConfigs: [ pokemonConfig ],
disableSwitch: true disableSwitch: true
}; };

View File

@ -4,23 +4,30 @@ import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; 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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; 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 { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils";
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; 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 i18next from "#app/plugins/i18n";
import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges"; 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 */ /** i18n namespace for encounter */
const namespace = "mysteryEncounters/weirdDream"; const namespace = "mysteryEncounters/weirdDream";
@ -80,10 +87,11 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [
const SUPER_LEGENDARY_BST_THRESHOLD = 600; const SUPER_LEGENDARY_BST_THRESHOLD = 600;
const NON_LEGENDARY_BST_THRESHOLD = 570; const NON_LEGENDARY_BST_THRESHOLD = 570;
const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450;
const OLD_GATEAU_STATS_UP = 20;
/** 0-100 */ /** 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 * 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) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM)
.withEncounterTier(MysteryEncounterTier.ROGUE) .withEncounterTier(MysteryEncounterTier.ROGUE)
.withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION) .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([ .withIntroSpriteConfigs([
{ {
spriteKey: "weird_dream_woman", spriteKey: "weird_dream_woman",
@ -131,6 +140,15 @@ export const WeirdDreamEncounter: MysteryEncounter =
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)
.withOnInit((scene: BattleScene) => { .withOnInit((scene: BattleScene) => {
scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); 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; return true;
}) })
.withOnVisualsStart((scene: BattleScene) => { .withOnVisualsStart((scene: BattleScene) => {
@ -156,13 +174,10 @@ export const WeirdDreamEncounter: MysteryEncounter =
doShowDreamBackground(scene); doShowDreamBackground(scene);
}); });
// Calculate all the newly transformed Pokemon and begin asset load for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) {
const teamTransformations = getTeamTransformations(scene); scene.removePokemonFromPlayerParty(transformation.previousPokemon, false);
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); scene.getParty().push(transformation.newPokemon);
scene.currentBattle.mysteryEncounter!.misc = { }
teamTransformations,
loadAssets
};
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue // 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 showEncounterText(scene, `${namespace}:option.1.dream_complete`);
await doNewTeamPostProcess(scene, transformations); 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); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -209,7 +224,88 @@ export const WeirdDreamEncounter: MysteryEncounter =
], ],
}, },
async (scene: BattleScene) => { 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()) { for (const pokemon of scene.getParty()) {
pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1);
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
@ -235,7 +331,7 @@ interface PokemonTransformation {
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
const party = scene.getParty(); const party = scene.getParty();
// Removes all pokemon from the party // Removes all pokemon from the party
const alreadyUsedSpecies: PokemonSpecies[] = []; const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species);
const pokemonTransformations: PokemonTransformation[] = party.map(p => { const pokemonTransformations: PokemonTransformation[] = party.map(p => {
return { return {
previousPokemon: p 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 // 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 // Then, roll the remainder of the party members at a +40 to +50 BST difference
const numPokemon = party.length; const numPokemon = party.length;
const removedPokemon = randSeedShuffle(party.slice(0));
for (let i = 0; i < numPokemon; i++) { 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); const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removed, false);
const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0); const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0);
let newBstRange: [number, number]; let newBstRange: [number, number];
@ -276,14 +372,13 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
pokemonTransformations[index].newSpecies = newSpecies; pokemonTransformations[index].newSpecies = newSpecies;
console.log("New species: " + JSON.stringify(newSpecies));
alreadyUsedSpecies.push(newSpecies); alreadyUsedSpecies.push(newSpecies);
} }
for (const transformation of pokemonTransformations) { for (const transformation of pokemonTransformations) {
const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount());
const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); transformation.newPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
transformation.newPokemon = newPlayerPokemon;
scene.getParty().push(newPlayerPokemon);
} }
return pokemonTransformations; return pokemonTransformations;
@ -296,109 +391,20 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
const newPokemon = transformation.newPokemon; const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId(); const speciesRootForm = newPokemon.species.getRootSpeciesId();
// Roll HA a second time if (await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, speciesRootForm)) {
if (newPokemon.species.abilityHidden) { atLeastOneNewStarter = true;
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 // Copy old items to new pokemon
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;
for (const item of transformation.heldItems) { for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id; item.pokemonId = newPokemon.id;
await scene.addModifier(item, false, false, false, true); await scene.addModifier(item, false, false, false, true);
} }
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
// 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 (shouldGetOldGateau(newPokemon)) {
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) { const stats = getOldGateauBoostedStats(newPokemon);
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);
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() 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); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon); const modifier = modType?.newModifier(newPokemon);
if (modifier) { 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(); newPokemon.calculateStats();
await newPokemon.updateInfo(); 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 { function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies {
let newSpecies: PokemonSpecies | undefined; let newSpecies: PokemonSpecies | undefined;
while (isNullOrUndefined(newSpecies)) { while (isNullOrUndefined(newSpecies)) {
@ -550,7 +685,7 @@ function doSideBySideTransformations(scene: BattleScene, transformations: Pokemo
* @param newPokemon * @param newPokemon
* @param speciesRootForm * @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; let eggMoveIndex: null | number = null;
const eggMoves = newPokemon.getEggMoves()?.slice(0); const eggMoves = newPokemon.getEggMoves()?.slice(0);
if (eggMoves) { 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 // 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); 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]; 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 { 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(); 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 { override meetsRequirement(scene: BattleScene): boolean {
for (const req of this.orRequirements) { return this.isAnd
if (req.meetsRequirement(scene)) { ? this.requirements.every(req => req.meetsRequirement(scene))
return true; : this.requirements.some(req => req.meetsRequirement(scene));
}
}
return false;
} }
/**
* 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] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
for (const req of this.orRequirements) { if (this.isAnd) {
if (req.meetsRequirement(scene)) { throw new Error("Not implemented (Sorry)");
return req.getDialogueToken(scene, pokemon); } 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]; 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 { 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(); super();
this.isAnd = isAnd;
this.invertQuery = false; this.invertQuery = false;
this.minNumberOfPokemon = 1; 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 { override meetsRequirement(scene: BattleScene): boolean {
for (const req of this.orRequirements) { return this.isAnd
if (req.meetsRequirement(scene)) { ? this.requirements.every(req => req.meetsRequirement(scene))
return true; : this.requirements.some(req => req.meetsRequirement(scene));
}
}
return false;
} }
/**
* 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[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
for (const req of this.orRequirements) { if (this.isAnd) {
const result = req.queryParty(partyPokemon); return this.requirements.reduce((relevantPokemon, req) => req.queryParty(relevantPokemon), partyPokemon);
if (result?.length > 0) { } else {
return result; 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] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
for (const req of this.orRequirements) { if (this.isAnd) {
if (req.meetsRequirement(scene)) { throw new Error("Not implemented (Sorry)");
return req.getDialogueToken(scene, pokemon); } 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; hasBattleAnimationsWithoutTargets: boolean;
skipEnemyBattleTurns: boolean; skipEnemyBattleTurns: boolean;
skipToFightInput: boolean; skipToFightInput: boolean;
preventGameStatsUpdates: boolean;
onInit?: (scene: BattleScene) => boolean; onInit?: (scene: BattleScene) => boolean;
onVisualsStart?: (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 * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu
*/ */
skipToFightInput: boolean; skipToFightInput: boolean;
/**
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
*/
preventGameStatsUpdates: boolean;
// #region Event callback functions // #region Event callback functions
@ -548,6 +553,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
hasBattleAnimationsWithoutTargets: boolean = false; hasBattleAnimationsWithoutTargets: boolean = false;
skipEnemyBattleTurns: boolean = false; skipEnemyBattleTurns: boolean = false;
skipToFightInput: boolean = false; skipToFightInput: boolean = false;
preventGameStatsUpdates: boolean = false;
maxAllowedEncounters: number = 3; maxAllowedEncounters: number = 3;
expMultiplier: number = 1; expMultiplier: number = 1;
@ -735,6 +741,14 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return Object.assign(this, { skipToFightInput }); 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 * Sets the maximum number of times that an encounter can spawn in a given Classic run
* @param maxAllowedEncounters * @param maxAllowedEncounters

View File

@ -118,3 +118,20 @@ export const EXTORTION_ABILITIES = [
Abilities.SUCTION_CUPS, Abilities.SUCTION_CUPS,
Abilities.STICKY_HOLD 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 { PermanentStat } from "#enums/stat";
import { VictoryPhase } from "#app/phases/victory-phase"; import { VictoryPhase } from "#app/phases/victory-phase";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; 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 */ /** Will give +1 level every 10 waves */
export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1;
@ -833,3 +835,21 @@ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scen
return null; 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.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.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.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.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.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), 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.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 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("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("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, 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, 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, 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, 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, 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, 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, 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, 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.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), 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 { StatusEffect } from "#enums/status-effect";
import i18next, { ParseKeys } from "i18next"; import i18next, { ParseKeys } from "i18next";
@ -6,17 +6,21 @@ export { StatusEffect };
export class Status { export class Status {
public effect: StatusEffect; public effect: StatusEffect;
public turnCount: integer; /** Toxic damage is `1/16 max HP * toxicTurnCount` */
public cureTurn: integer | null; 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.effect = effect;
this.turnCount = turnCount === undefined ? 0 : turnCount; this.toxicTurnCount = toxicTurnCount;
this.cureTurn = cureTurn!; // TODO: is this bang correct? this.sleepTurnsRemaining = sleepTurnsRemaining;
} }
incrementTurn(): void { incrementTurn(): void {
this.turnCount++; this.toxicTurnCount++;
if (this.sleepTurnsRemaining) {
this.sleepTurnsRemaining--;
}
} }
isPostTurn(): boolean { isPostTurn(): boolean {
@ -107,7 +111,7 @@ export function getStatusEffectCatchRateMultiplier(statusEffect: StatusEffect):
* Returns a random non-volatile StatusEffect * Returns a random non-volatile StatusEffect
*/ */
export function generateRandomStatusEffect(): 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 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) [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)), .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)),
[TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder") [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, VITO,
BUG_TYPE_SUPERFAN, BUG_TYPE_SUPERFAN,
EXPERT_POKEMON_BREEDER, EXPERT_POKEMON_BREEDER,
FUTURE_SELF_M,
FUTURE_SELF_F,
BROCK = 200, BROCK = 200,
MISTY, MISTY,

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; 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 { 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 { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives"; 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 { 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 { WeatherType } from "#app/data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; 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 PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
@ -93,7 +93,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public stats: integer[]; public stats: integer[];
public ivs: integer[]; public ivs: integer[];
public nature: Nature; public nature: Nature;
public natureOverride: Nature | -1;
public moveset: (PokemonMove | null)[]; public moveset: (PokemonMove | null)[];
public status: Status | null; public status: Status | null;
public friendship: integer; 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 // 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) ]); 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 // 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. // 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); 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 { getOpponent(targetIndex: integer): Pokemon | null {
const ret = this.getOpponents()[targetIndex]; const ret = this.getOpponents()[targetIndex];
if (ret.summonData) { 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) */ /** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */
const screenMultiplier = new Utils.NumberHolder(1); 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: * 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); if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
return false; return false;
} }
const types = this.getTypes(true, true);
switch (effect) { switch (effect) {
case StatusEffect.POISON: case StatusEffect.POISON:
case StatusEffect.TOXIC: case StatusEffect.TOXIC:
@ -3422,7 +3429,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return true; 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)) { if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) {
return false; return false;
} }
@ -3436,15 +3443,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
if (asPhase) { 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; return true;
} }
let statusCureTurn: Utils.IntegerHolder; let sleepTurnsRemaining: Utils.NumberHolder;
if (effect === StatusEffect.SLEEP) { if (effect === StatusEffect.SLEEP) {
statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4)); sleepTurnsRemaining = new Utils.NumberHolder(this.randSeedIntRange(2, 4));
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, false, effect, statusCureTurn);
this.setFrameRate(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 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) { if (effect !== StatusEffect.FAINT) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); 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 { primeSummonData(summonDataPrimer: PokemonSummonData): void {
this.summonDataPrimer = summonDataPrimer; 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); super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
if (Overrides.STATUS_OVERRIDE) { if (Overrides.STATUS_OVERRIDE) {
this.status = new Status(Overrides.STATUS_OVERRIDE); this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4);
} }
if (Overrides.SHINY_OVERRIDE) { if (Overrides.SHINY_OVERRIDE) {
@ -4259,7 +4282,6 @@ export class PlayerPokemon extends Pokemon {
if (newEvolution.condition?.predicate(this)) { 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); 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.passive = this.passive;
newPokemon.moveset = this.moveset.slice(); newPokemon.moveset = this.moveset.slice();
newPokemon.moveset = this.copyMoveset(); newPokemon.moveset = this.copyMoveset();
@ -4409,7 +4431,7 @@ export class PlayerPokemon extends Pokemon {
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex); this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0]; this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = this.scene.getParty().indexOf(this); 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(); pokemon.destroy();
this.updateFusionPalette(); this.updateFusionPalette();
resolve(); resolve();
@ -4430,8 +4452,12 @@ export class PlayerPokemon extends Pokemon {
/** Returns a deep copy of this Pokemon's moveset array */ /** Returns a deep copy of this Pokemon's moveset array */
copyMoveset(): PokemonMove[] { copyMoveset(): PokemonMove[] {
const newMoveset : PokemonMove[] = []; const newMoveset : PokemonMove[] = [];
this.moveset.forEach(move => this.moveset.forEach((move) => {
newMoveset.push(new PokemonMove(move!.moveId, 0, move!.ppUp, move!.virtual))); // TODO: are those bangs correct? // 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; return newMoveset;
} }
@ -4456,7 +4482,7 @@ export class EnemyPokemon extends Pokemon {
} }
if (Overrides.OPP_STATUS_OVERRIDE) { 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) { if (Overrides.OPP_GENDER_OVERRIDE) {
@ -4465,9 +4491,11 @@ export class EnemyPokemon extends Pokemon {
const speciesId = this.species.speciesId; const speciesId = this.species.speciesId;
if (speciesId in Overrides.OPP_FORM_OVERRIDES if (
speciesId in Overrides.OPP_FORM_OVERRIDES
&& Overrides.OPP_FORM_OVERRIDES[speciesId] && 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; this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId] ?? 0;
} }
@ -5153,15 +5181,22 @@ export interface DamageCalculationResult {
**/ **/
export class PokemonMove { export class PokemonMove {
public moveId: Moves; public moveId: Moves;
public ppUsed: integer; public ppUsed: number;
public ppUp: integer; public ppUp: number;
public virtual: boolean; 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.moveId = moveId;
this.ppUsed = ppUsed || 0; this.ppUsed = ppUsed;
this.ppUp = ppUp || 0; this.ppUp = ppUp;
this.virtual = !!virtual; this.virtual = virtual;
this.maxPpOverride = maxPpOverride;
} }
/** /**
@ -5198,7 +5233,7 @@ export class PokemonMove {
} }
getMovePp(): integer { 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 { getPpRatio(): number {
@ -5215,6 +5250,6 @@ export class PokemonMove {
* @return {PokemonMove} A valid pokemonmove object * @return {PokemonMove} A valid pokemonmove object
*/ */
static loadMove(source: PokemonMove | any): PokemonMove { 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("discord", "ui");
this.loadImage("google", "ui"); this.loadImage("google", "ui");
this.loadImage("settings_icon", "ui"); this.loadImage("settings_icon", "ui");
this.loadImage("link_icon", "ui");
this.loadImage("unlink_icon", "ui");
this.loadImage("default_bg", "arenas"); this.loadImage("default_bg", "arenas");
// Load arena images // Load arena images

View File

@ -10,7 +10,9 @@ import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; 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 { ModifierTier } from "#app/modifier/modifier-tier";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { Unlockables } from "#app/system/unlockables"; import { Unlockables } from "#app/system/unlockables";
@ -382,7 +384,7 @@ export class PokemonPpUpModifierType extends PokemonMoveModifierType {
(_pokemon: PlayerPokemon) => { (_pokemon: PlayerPokemon) => {
return null; return null;
}, (pokemonMove: PokemonMove) => { }, (pokemonMove: PokemonMove) => {
if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3) { if (pokemonMove.getMove().pp < 5 || pokemonMove.ppUp >= 3 || pokemonMove.maxPpOverride) {
return PartyUiHandler.NoEffectMessage; return PartyUiHandler.NoEffectMessage;
} }
return null; return null;
@ -1561,6 +1563,7 @@ export const modifierTypes = {
VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1), 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"), 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_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)), 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) { if (pregenArgs) {
return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number); 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[]) => { MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) { if (pregenArgs) {
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); 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[]) => { MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) { if (pregenArgs) {

View File

@ -404,6 +404,14 @@ export abstract class LapsingPersistentModifier extends PersistentModifier {
this.battleCount = this.maxBattles; 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 { getMaxBattles(): number {
return this.maxBattles; return this.maxBattles;
} }
@ -960,7 +968,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
this.stackCount = pokemon this.stackCount = pokemon
? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length ? 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; : this.stackCount;
const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); 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 { getMaxHeldItemCount(pokemon: Pokemon): number {
this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length 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; return 999;
} }
} }
@ -2158,7 +2166,7 @@ export class PokemonPpUpModifier extends ConsumablePokemonMoveModifier {
override apply(playerPokemon: PlayerPokemon): boolean { override apply(playerPokemon: PlayerPokemon): boolean {
const move = playerPokemon.getMoveset()[this.moveIndex]; const move = playerPokemon.getMoveset()[this.moveIndex];
if (move) { if (move && !move.maxPpOverride) {
move.ppUp = Math.min(move.ppUp + this.upPoints, 3); 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 { export abstract class EnemyPersistentModifier extends PersistentModifier {
constructor(type: ModifierType, stackCount?: number) { constructor(type: ModifierType, stackCount?: number) {
super(type, stackCount); super(type, stackCount);

View File

@ -75,6 +75,8 @@ class DefaultOverrides {
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = []; readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
/** Set to `true` to show all tutorials */ /** Set to `true` to show all tutorials */
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false; 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 // PLAYER OVERRIDES

View File

@ -30,6 +30,15 @@ export class CommandPhase extends FieldPhase {
start() { start() {
super.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 (this.fieldIndex) {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out // 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 // 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 { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; 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 { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
@ -24,10 +24,10 @@ export class MoveEffectPhase extends PokemonPhase {
super(scene, battlerIndex); super(scene, battlerIndex);
this.move = move; this.move = move;
/** /**
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies * 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 * 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. * of the left Pokemon and gets hit unless this is checked.
*/ */
if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
const i = targets.indexOf(battlerIndex); const i = targets.indexOf(battlerIndex);
targets.splice(i, i + 1); 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? * Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use. * e.g. Charging moves (Fly, etc.) on their first turn of use.
*/ */
const overridden = new Utils.BooleanHolder(false); const overridden = new Utils.BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove(); const move = this.move.getMove();
@ -66,10 +66,10 @@ export class MoveEffectPhase extends PokemonPhase {
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
/** /**
* If this phase is for the first hit of the invoked move, * If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the * resolve the move's total hit count. This block combines the
* effects of the move itself, Parental Bond, and Multi-Lens to do so. * effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/ */
if (user.turnData.hitsLeft === -1) { if (user.turnData.hitsLeft === -1) {
const hitCount = new Utils.IntegerHolder(1); const hitCount = new Utils.IntegerHolder(1);
// Assume single target for multi hit // 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. * 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 * 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?". * 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 }; 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. * Stores results of hit checks of the invoked move against all targets, organized by battler index.
* @see {@linkcode hitCheck} * @see {@linkcode hitCheck}
*/ */
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
const hasActiveTargets = targets.some(t => t.isActive(true)); const hasActiveTargets = targets.some(t => t.isActive(true));
@ -104,11 +104,10 @@ export class MoveEffectPhase extends PokemonPhase {
&& !targets[0].getTag(SemiInvulnerableTag); && !targets[0].getTag(SemiInvulnerableTag);
/** /**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target * 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 * (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. * 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)) { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
@ -154,9 +153,9 @@ export class MoveEffectPhase extends PokemonPhase {
&& !target.getTag(SemiInvulnerableTag); && !target.getTag(SemiInvulnerableTag);
/** /**
* If the move missed a target, stop all future hits against that target * If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one). * and move on to the next target (if there is one).
*/ */
if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) { if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) {
this.stopMultiHit(target); this.stopMultiHit(target);
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(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. * 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 * 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. * is overwritten and the move is logged as a FAIL.
*/ */
moveHistoryEntry.result = MoveResult.SUCCESS; moveHistoryEntry.result = MoveResult.SUCCESS;
/** /**
* Stores the result of applying the invoked move to the target. * Stores the result of applying the invoked move to the target.
* If the target is protected, the result is always `NO_EFFECT`. * If the target is protected, the result is always `NO_EFFECT`.
* Otherwise, the hit result is based on type effectiveness, immunities, * Otherwise, the hit result is based on type effectiveness, immunities,
* and other factors that may negate the attack or status application. * and other factors that may negate the attack or status application.
* *
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated * 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 * (for attack moves) and the target's HP is updated. However, this isn't
* made visible to the user until the resulting {@linkcode DamagePhase} * made visible to the user until the resulting {@linkcode DamagePhase}
* is invoked. * is invoked.
*/ */
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ /** 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), * If the move has no effect on the target (i.e. the target is protected or immune),
* change the logged move result to FAIL. * change the logged move result to FAIL.
*/ */
if (hitResult === HitResult.NO_EFFECT) { if (hitResult === HitResult.NO_EFFECT) {
moveHistoryEntry.result = MoveResult.FAIL; moveHistoryEntry.result = MoveResult.FAIL;
} }
@ -222,43 +221,41 @@ export class MoveEffectPhase extends PokemonPhase {
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
/** /**
* If the user can change forms by using the invoked move, * If the user can change forms by using the invoked move,
* it only changes forms after the move's last hit * it only changes forms after the move's last hit
* (see Relic Song's interaction with Parental Bond when used by Meloetta). * (see Relic Song's interaction with Parental Bond when used by Meloetta).
*/ */
if (lastHit) { if (lastHit) {
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
} }
/** /**
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. * 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 * 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. * type requires different conditions to be met with respect to the move's hit result.
*/ */
applyAttrs.push(new Promise(resolve => { applyAttrs.push(new Promise(resolve => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) // 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, 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(() => { user, target, move).then(() => {
// All other effects require the move to not have failed or have been cancelled to trigger // All other effects require the move to not have failed or have been cancelled to trigger
if (hitResult !== HitResult.FAIL) { 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," * If the invoked move's effects are meant to trigger during the move's "charge turn,"
* ignore all effects after this point. * ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects. * Otherwise, apply all self-targeted POST_APPLY effects.
*/ */
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { && 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 // All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) { if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects // Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY 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(() => { && !(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, * If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock * apply the chance to flinch the target gained from King's Rock
*/ */
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false); const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); 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 // 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(() => { && (!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) // 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(() => { 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) { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
} }
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); target.lapseTags(BattlerTagLapseType.AFTER_HIT);
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP);
}
})).then(() => { })).then(() => {
// Apply the user's post-attack ability effects // Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
/** /**
* If the invoked move is an attack, apply the user's chance to * If the invoked move is an attack, apply the user's chance to
* steal an item from the target granted by Grip Claw * steal an item from the target granted by Grip Claw
*/ */
if (this.move.getMove() instanceof AttackMove) { if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
} }
@ -345,12 +340,12 @@ export class MoveEffectPhase extends PokemonPhase {
end() { end() {
const user = this.getUserPokemon(); const user = this.getUserPokemon();
/** /**
* If this phase isn't for the invoked move's last strike, * If this phase isn't for the invoked move's last strike,
* unshift another MoveEffectPhase for the next strike. * unshift another MoveEffectPhase for the next strike.
* Otherwise, queue a message indicating the number of times the move has struck * 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 * (if the move has struck more than once), then apply the heal from Shell Bell
* to the user. * to the user.
*/ */
if (user) { if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase()); 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 * Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed * @param target {@linkcode Pokemon} the Pokemon to be removed
*/ */
removeTarget(target: Pokemon): void { removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) { if (targetIndex !== -1) {
@ -460,19 +455,19 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** /**
* Prevents subsequent strikes of this phase's invoked move from occurring * Prevents subsequent strikes of this phase's invoked move from occurring
* @param target {@linkcode Pokemon} if defined, only stop subsequent * @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon * strikes against this Pokemon
*/ */
stopMultiHit(target?: Pokemon): void { stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */ /** If given a specific target, remove the target from subsequent strikes */
if (target) { if (target) {
this.removeTarget(target); this.removeTarget(target);
} }
/** /**
* If no target specified, or the specified target was the last of this move's * If no target specified, or the specified target was the last of this move's
* targets, completely cancel all subsequent strikes. * targets, completely cancel all subsequent strikes.
*/ */
if (!target || this.targets.length === 0 ) { if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here?
this.getUserPokemon()!.turnData.hitsLeft = 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 { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene"; 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 { CommonAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; 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 { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import { getTerrainBlockMessage } from "#app/data/weather"; import { getTerrainBlockMessage } from "#app/data/weather";
import { MoveUsedEvent } from "#app/events/battle-scene"; 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 { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase"; import { BattlePhase } from "#app/phases/battle-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-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 { Moves } from "#enums/moves";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { MoveChargePhase } from "#app/phases/move-charge-phase";
export class MovePhase extends BattlePhase { export class MovePhase extends BattlePhase {
protected _pokemon: Pokemon; protected _pokemon: Pokemon;
@ -134,6 +136,8 @@ export class MovePhase extends BattlePhase {
if (this.cancelled || this.failed) { if (this.cancelled || this.failed) {
this.handlePreMoveFailures(); this.handlePreMoveFailures();
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
this.chargeMove();
} else { } else {
this.useMove(); this.useMove();
} }
@ -168,25 +172,31 @@ export class MovePhase extends BattlePhase {
switch (this.pokemon.status.effect) { switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS: case StatusEffect.PARALYSIS:
if (!this.pokemon.randSeedInt(4)) { activated = (!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
activated = true;
this.cancelled = true;
}
break; break;
case StatusEffect.SLEEP: case StatusEffect.SLEEP:
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); 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); activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
this.cancelled = activated;
break; break;
case StatusEffect.FREEZE: 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; activated = !healed;
this.cancelled = activated;
break; break;
} }
if (activated) { if (activated) {
this.cancel();
this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); 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))); this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
} else if (healed) { } else if (healed) {
@ -219,12 +229,15 @@ export class MovePhase extends BattlePhase {
this.showMoveText(); this.showMoveText();
// TODO: Clean up implementation of two-turn moves.
if (moveQueue.length > 0) { if (moveQueue.length > 0) {
// Using .shift here clears out two turn moves once they've been used // Using .shift here clears out two turn moves once they've been used
this.ignorePp = moveQueue.shift()?.ignorePP ?? false; 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. // "commit" to using the move, deducting PP.
if (!this.ignorePp) { if (!this.ignorePp) {
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
@ -288,6 +301,9 @@ export class MovePhase extends BattlePhase {
} }
this.showFailedText(failedText); 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()`). // 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`, * Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`,
* then ends the phase. * then ends the phase.
@ -412,8 +457,6 @@ export class MovePhase extends BattlePhase {
* - Lapses `AFTER_MOVE` tags: * - Lapses `AFTER_MOVE` tags:
* - This handles the effects of {@link Moves.SUBSTITUTE Substitute} * - This handles the effects of {@link Moves.SUBSTITUTE Substitute}
* - Removes the second turn of charge moves * - Removes the second turn of charge moves
*
* TODO: handle charge moves more gracefully
*/ */
protected handlePreMoveFailures(): void { protected handlePreMoveFailures(): void {
if (this.cancelled || this.failed) { if (this.cancelled || this.failed) {
@ -445,18 +488,7 @@ export class MovePhase extends BattlePhase {
return; return;
} }
if (this.move.getMove().hasAttr(ChargeAttr)) { if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) {
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)) {
return; return;
} }

View File

@ -8,26 +8,26 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
export class ObtainStatusEffectPhase extends PokemonPhase { export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect | undefined; private statusEffect?: StatusEffect;
private cureTurn?: integer | null; private turnsRemaining?: number;
private sourceText?: string | null; private sourceText?: string | null;
private sourcePokemon?: Pokemon | 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); super(scene, battlerIndex);
this.statusEffect = statusEffect; this.statusEffect = statusEffect;
this.cureTurn = cureTurn; this.turnsRemaining = turnsRemaining;
this.sourceText = sourceText; this.sourceText = sourceText;
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect this.sourcePokemon = sourcePokemon;
} }
start() { start() {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (pokemon && !pokemon.status) { if (pokemon && !pokemon.status) {
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (this.cureTurn) { if (this.turnsRemaining) {
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct? pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
} }
pokemon.updateInfo(true); pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => { 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(); const pokemon = this.getPokemon();
if (pokemon.status?.effect === StatusEffect.TOXIC) { if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.turnCount = 0; pokemon.status.toxicTurnCount = 0;
} }
this.scene.arena.applyTags(ArenaTrapTag, false, pokemon); 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); damage.value = Math.max(pokemon.getMaxHp() >> 3, 1);
break; break;
case StatusEffect.TOXIC: 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; break;
case StatusEffect.BURN: case StatusEffect.BURN:
damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); damage.value = Math.max(pokemon.getMaxHp() >> 4, 1);

View File

@ -1,7 +1,7 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { ModifierTier } from "#app/modifier/modifier-tier"; 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 { 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 ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
@ -45,6 +45,7 @@ export class SelectModifierPhase extends BattlePhase {
const modifierCount = new Utils.IntegerHolder(3); const modifierCount = new Utils.IntegerHolder(3);
if (this.isPlayer()) { if (this.isPlayer()) {
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount);
} }
// If custom modifiers are specified, overrides default item count // If custom modifiers are specified, overrides default item count
@ -274,7 +275,13 @@ export class SelectModifierPhase extends BattlePhase {
// Otherwise, continue with custom multiplier // Otherwise, continue with custom multiplier
multiplier = this.customModifierSettings.rerollMultiplier; 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 { getPoolType(): ModifierPoolType {

View File

@ -64,7 +64,8 @@ export class StatStageChangePhase extends PokemonPhase {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (!this.selfTarget && stages.value < 0) { 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) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {

View File

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

View File

@ -25,12 +25,17 @@ export class VictoryPhase extends PokemonPhase {
start() { start() {
super.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(); const expValue = this.getPokemon().getExpValue();
this.scene.applyPartyExp(expValue, true); this.scene.applyPartyExp(expValue, true);
if (this.scene.currentBattle.isBattleMysteryEncounter()) { if (isMysteryEncounter) {
handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); handleMysteryEncounterVictory(this.scene, false, this.isExpOnly);
return this.end(); return this.end();
} }

View File

@ -1569,6 +1569,10 @@ export class GameData {
} }
setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void { 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]; const dexEntry = this.dexData[pokemon.species.speciesId];
dexEntry.seenAttr |= pokemon.getDexAttr(); dexEntry.seenAttr |= pokemon.getDexAttr();
if (incrementCount) { if (incrementCount) {

View File

@ -92,7 +92,6 @@ export default class PokemonData {
this.stats = source.stats; this.stats = source.stats;
this.ivs = source.ivs; this.ivs = source.ivs;
this.nature = source.nature !== undefined ? source.nature : 0 as Nature; 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.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship;
this.metLevel = source.metLevel || 5; this.metLevel = source.metLevel || 5;
this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; this.metBiome = source.metBiome !== undefined ? source.metBiome : -1;
@ -117,6 +116,8 @@ export default class PokemonData {
this.customPokemonData = new CustomPokemonData(source.customPokemonData); this.customPokemonData = new CustomPokemonData(source.customPokemonData);
// Deprecated, but needed for session data migration
this.natureOverride = source.natureOverride;
this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData); this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData);
this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData); this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData);
@ -134,10 +135,10 @@ export default class PokemonData {
} }
} }
} else { } 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) { if (!forHistory) {
this.status = source.status 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; : 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 () => { it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
@ -62,25 +60,24 @@ describe("Abilities - Imposter", () => {
const playerMoveset = player.getMoveset(); const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset(); const enemyMoveset = player.getMoveset();
expect(playerMoveset.length).toBe(enemyMoveset.length);
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { 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); expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
} }
const playerTypes = player.getTypes(); const playerTypes = player.getTypes();
const enemyTypes = enemy.getTypes(); const enemyTypes = enemy.getTypes();
expect(playerTypes.length).toBe(enemyTypes.length);
for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
expect(playerTypes[i]).toBe(enemyTypes[i]); expect(playerTypes[i]).toBe(enemyTypes[i]);
} }
}, 20000); });
it("should copy in-battle overridden stats", async () => { it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([ Moves.POWER_SPLIT ]); game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
@ -97,4 +94,26 @@ describe("Abilities - Imposter", () => {
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.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 enemyPokemon = game.scene.getEnemyPokemon()!;
const toxicStartCounter = enemyPokemon.status!.turnCount; const toxicStartCounter = enemyPokemon.status!.toxicTurnCount;
//should be 0 //should be 0
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
@ -162,7 +162,7 @@ describe("Abilities - Magic Guard", () => {
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 * - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
*/ */
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); 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); 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(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
}); });
it("should activate regardless of accuracy checks", async () => { it("should activate regardless of accuracy checks", async () => {
game.override.moveset(Moves.THUNDERBOLT); game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.SPLASH); game.override.enemyMoveset(Moves.SPLASH);
@ -71,6 +72,7 @@ describe("Abilities - Volt Absorb", () => {
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}); });
it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => { it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => {
game.override.moveset(Moves.THUNDERBOLT); game.override.moveset(Moves.THUNDERBOLT);
game.override.enemyMoveset(Moves.DIVE); game.override.enemyMoveset(Moves.DIVE);
@ -84,9 +86,7 @@ describe("Abilities - Volt Absorb", () => {
game.move.select(Moves.THUNDERBOLT); game.move.select(Moves.THUNDERBOLT);
enemyPokemon.hp = enemyPokemon.hp - 1; enemyPokemon.hp = enemyPokemon.hp - 1;
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceMiss();
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
}); });

View File

@ -1,8 +1,8 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#app/enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { BattlerTagType } from "#enums/battler-tag-type";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
@ -31,7 +31,8 @@ describe("Arena - Gravity", () => {
.ability(Abilities.UNNERVE) .ability(Abilities.UNNERVE)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.enemySpecies(Species.SHUCKLE) .enemySpecies(Species.SHUCKLE)
.enemyMoveset(Moves.SPLASH); .enemyMoveset(Moves.SPLASH)
.enemyLevel(5);
}); });
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) // Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
@ -42,102 +43,121 @@ describe("Arena - Gravity", () => {
vi.spyOn(moveToCheck, "calculateBattleAccuracy"); vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn // Setup Gravity on first turn
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use non-OHKO move on second turn // Use non-OHKO move on second turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.TACKLE); 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 () => { 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)} */ /** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
const moveToCheck = allMoves[Moves.FISSURE]; const moveToCheck = allMoves[Moves.FISSURE];
vi.spyOn(moveToCheck, "calculateBattleAccuracy"); vi.spyOn(moveToCheck, "calculateBattleAccuracy");
// Setup Gravity on first turn // Setup Gravity on first turn
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use OHKO move on second turn // Use OHKO move on second turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.FISSURE); 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", () => { describe("Against flying types", () => {
it("can be hit by ground-type moves now", async () => { it("can be hit by ground-type moves now", async () => {
game.override game.override
.startingLevel(5)
.enemyLevel(5)
.enemySpecies(Species.PIDGEOT) .enemySpecies(Species.PIDGEOT)
.moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]); .moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]);
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
const pidgeot = game.scene.getEnemyPokemon()!; const pidgeot = game.scene.getEnemyPokemon()!;
vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
// Try earthquake on 1st turn (fails!); // Try earthquake on 1st turn (fails!);
game.move.select(Moves.EARTHQUAKE); 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 // Setup Gravity on 2nd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use ground move on 3rd turn // Use ground move on 3rd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.EARTHQUAKE); 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 () => { it("keeps super-effective moves super-effective after using gravity", async () => {
game.override game.override
.startingLevel(5)
.enemyLevel(5)
.enemySpecies(Species.PIDGEOT) .enemySpecies(Species.PIDGEOT)
.moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]); .moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]);
await game.startBattle([ Species.PIKACHU ]); await game.classicMode.startBattle([ Species.PIKACHU ]);
const pidgeot = game.scene.getEnemyPokemon()!; const pidgeot = game.scene.getEnemyPokemon()!;
vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
// Setup Gravity on 1st turn // Setup Gravity on 1st turn
game.move.select(Moves.GRAVITY); game.move.select(Moves.GRAVITY);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
// Use electric move on 2nd turn // Use electric move on 2nd turn
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.THUNDERBOLT); 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 { import {
Status,
StatusEffect, StatusEffect,
getStatusEffectActivationText, getStatusEffectActivationText,
getStatusEffectDescriptor, getStatusEffectDescriptor,
@ -6,14 +7,19 @@ import {
getStatusEffectObtainText, getStatusEffectObtainText,
getStatusEffectOverlapText, getStatusEffectOverlapText,
} from "#app/data/status-effect"; } 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 { mockI18next } from "#test/utils/testUtils";
import i18next from "i18next"; 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 pokemonName = "PKM";
const sourceText = "SOURCE"; const sourceText = "SOURCE";
describe("status-effect", () => { describe("Status Effect Messages", () => {
beforeAll(() => { beforeAll(() => {
i18next.init(); i18next.init();
}); });
@ -299,3 +305,99 @@ describe("status-effect", () => {
vi.resetAllMocks(); 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 Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Items - Toxic orb", () => { describe("Items - Toxic orb", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
@ -27,10 +25,10 @@ describe("Items - Toxic orb", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleType("single") .battleType("single")
.enemySpecies(Species.RATTATA) .enemySpecies(Species.MAGIKARP)
.ability(Abilities.BALL_FETCH) .ability(Abilities.BALL_FETCH)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.moveset([ Moves.SPLASH ]) .moveset(Moves.SPLASH)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.startingHeldItems([{ .startingHeldItems([{
name: "TOXIC_ORB", name: "TOXIC_ORB",
@ -39,22 +37,19 @@ describe("Items - Toxic orb", () => {
vi.spyOn(i18next, "t"); vi.spyOn(i18next, "t");
}); });
it("badly poisons the holder", async () => { it("should badly poison the holder", async () => {
await game.classicMode.startBattle([ Species.MIGHTYENA ]); 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); game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
// Toxic orb should trigger here await game.phaseInterceptor.to("MessagePhase");
await game.phaseInterceptor.run("MessagePhase");
expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything()); expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything());
await game.toNextTurn();
expect(player.status?.effect).toBe(StatusEffect.TOXIC); expect(player.status?.effect).toBe(StatusEffect.TOXIC);
// Damage should not have ticked yet. expect(player.status?.toxicTurnCount).toBe(0);
expect(player.status?.turnCount).toBe(0); });
}, TIMEOUT);
}); });

View File

@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) { 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; return move.power * multiplierHolder.value;

View File

@ -34,7 +34,7 @@ describe("Moves - Baton Pass", () => {
.disableCrits(); .disableCrits();
}); });
it("transfers all stat stages when player uses it", async() => { it("transfers all stat stages when player uses it", async () => {
// arrange // arrange
await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]);
@ -91,7 +91,7 @@ describe("Moves - Baton Pass", () => {
]); ]);
}, 20000); }, 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 ]); game.override.enemyMoveset([ Moves.SALT_CURE ]);
await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]); 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(); expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined();
}, 20000); }, 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; const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) { 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; 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; const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) { 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; 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 () => { it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
game.move.select(Moves.TRANSFORM); game.move.select(Moves.TRANSFORM);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
@ -62,25 +60,24 @@ describe("Moves - Transform", () => {
const playerMoveset = player.getMoveset(); const playerMoveset = player.getMoveset();
const enemyMoveset = player.getMoveset(); const enemyMoveset = player.getMoveset();
expect(playerMoveset.length).toBe(enemyMoveset.length);
for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { 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); expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId);
} }
const playerTypes = player.getTypes(); const playerTypes = player.getTypes();
const enemyTypes = enemy.getTypes(); const enemyTypes = enemy.getTypes();
expect(playerTypes.length).toBe(enemyTypes.length);
for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) {
expect(playerTypes[i]).toBe(enemyTypes[i]); expect(playerTypes[i]).toBe(enemyTypes[i]);
} }
}, 20000); });
it("should copy in-battle overridden stats", async () => { it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([ Moves.POWER_SPLIT ]); game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
await game.startBattle([ await game.classicMode.startBattle([ Species.DITTO ]);
Species.DITTO
]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
@ -97,4 +94,26 @@ describe("Moves - Transform", () => {
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
expect(enemy.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()!; const staraptor = game.scene.getPlayerPokemon()!;
game.move.select(move); game.move.select(move);
await game.toNextTurn();
await game.phaseInterceptor.to("BerryPhase", false);
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); 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); 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); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 1 Reviver Seed on party lead // Set 1 Reviver Seed on party lead
@ -220,11 +220,11 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); 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(reviverSeedAfter).toBeUndefined();
expect(healingCharmAfter).toBeDefined(); expect(berryPouchAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(1); expect(berryPouchAfter?.stackCount).toBe(1);
}); });
it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { 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); 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); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// 5 Healing Charms // 3 Berry Pouches
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
healingCharm.stackCount = 5; healingCharm.stackCount = 3;
await scene.addModifier(healingCharm, true, false, false, true); await scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Reviver Seed on party lead // Set 1 Reviver Seed on party lead
@ -275,12 +275,12 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); 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); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
expect(reviverSeedAfter).toBeUndefined(); expect(reviverSeedAfter).toBeUndefined();
expect(healingCharmAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(healingCharmAfter?.stackCount).toBe(5); expect(healingCharmAfter?.stackCount).toBe(3);
expect(shellBellAfter).toBeDefined(); expect(shellBellAfter).toBeDefined();
expect(shellBellAfter?.stackCount).toBe(1); 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); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 2 Soul Dew on party lead // Set 2 Soul Dew on party lead
@ -361,14 +361,14 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); 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(soulDewAfter?.stackCount).toBe(1);
expect(berryPouchAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1); 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); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// Set 1 Soul Dew on party lead // Set 1 Soul Dew on party lead
@ -382,20 +382,20 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); 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(soulDewAfter).toBeUndefined();
expect(berryPouchAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(1); 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); await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
// 5 Healing Charms // 5 Healing Charms
scene.modifiers = []; scene.modifiers = [];
const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
healingCharm.stackCount = 3; healingCharm.stackCount = 5;
await scene.addModifier(healingCharm, true, false, false, true); await scene.addModifier(healingCharm, true, false, false, true);
// Set 1 Soul Dew on party lead // Set 1 Soul Dew on party lead
@ -408,12 +408,12 @@ describe("Delibird-y - Mystery Encounter", () => {
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); 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); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
expect(soulDewAfter).toBeUndefined(); expect(soulDewAfter).toBeUndefined();
expect(berryPouchAfter).toBeDefined(); expect(healingCharmAfter).toBeDefined();
expect(berryPouchAfter?.stackCount).toBe(3); expect(healingCharmAfter?.stackCount).toBe(5);
expect(shellBellAfter).toBeDefined(); expect(shellBellAfter).toBeDefined();
expect(shellBellAfter?.stackCount).toBe(1); 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 { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene"; 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 { Type } from "#app/data/type";
import { Status, StatusEffect } from "#app/data/status-effect"; import { Status, StatusEffect } from "#app/data/status-effect";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; 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 { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-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"; import i18next from "i18next";
const namespace = "mysteryEncounters/fieryFallout"; const namespace = "mysteryEncounters/fieryFallout";
@ -42,10 +44,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
beforeEach(async () => { beforeEach(async () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100)
game.override.startingWave(defaultWave); .startingWave(defaultWave)
game.override.startingBiome(defaultBiome); .startingBiome(defaultBiome)
game.override.disableTrainerWaves(); .disableTrainerWaves()
.moveset([ Moves.PAYBACK, Moves.THUNDERBOLT ]); // Required for attack type booster item generation
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([
@ -109,12 +112,16 @@ describe("Fiery Fallout - Mystery Encounter", () => {
{ {
species: getPokemonSpecies(Species.VOLCARONA), species: getPokemonSpecies(Species.VOLCARONA),
isBoss: false, isBoss: false,
gender: Gender.MALE gender: Gender.MALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: expect.any(Function)
}, },
{ {
species: getPokemonSpecies(Species.VOLCARONA), species: getPokemonSpecies(Species.VOLCARONA),
isBoss: false, isBoss: false,
gender: Gender.FEMALE gender: Gender.FEMALE,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
mysteryEncounterBattleEffects: expect.any(Function)
} }
], ],
doubleBattle: true, doubleBattle: true,
@ -157,12 +164,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender 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]); 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.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 game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true); await runMysteryEncounterToEnd(game, 1, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game); await skipBattleRunMysteryEncounterRewardsPhase(game);
@ -172,8 +178,8 @@ describe("Fiery Fallout - Mystery Encounter", () => {
const leadPokemonId = scene.getParty()?.[0].id; const leadPokemonId = scene.getParty()?.[0].id;
const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[];
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier);
expect(charcoal).toBeDefined; 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); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
const party = scene.getParty(); const party = scene.getParty();
@ -210,7 +216,8 @@ describe("Fiery Fallout - Mystery Encounter", () => {
burnablePokemon.forEach((pkm) => { 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(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())); 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 game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
await runMysteryEncounterToEnd(game, 3); await runMysteryEncounterToEnd(game, 3);
await game.phaseInterceptor.to(SelectModifierPhase, false); await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const leadPokemonId = scene.getParty()?.[0].id; const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[];
const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier);
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; expect(item).toBeDefined;
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal");
expect(charcoal).toBeDefined;
}); });
it("should leave encounter without battle", async () => { 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 () => { 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); await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const encounterPhase = scene.getCurrentPhase(); const encounterPhase = scene.getCurrentPhase();

View File

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

View File

@ -5,7 +5,7 @@ import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager"; import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; 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 BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; 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 { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter";
import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; 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 namespace = "mysteryEncounters/weirdDream";
const defaultParty = [ Species.MAGBY, Species.HAUNTER, Species.ABRA ]; 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?.title).toBe(`${namespace}:title`);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`); expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`); 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 () => { it("should initialize fully", async () => {
@ -132,7 +134,7 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(plus40To50.length).toBe(1); 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 game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 1); await runMysteryEncounterToEnd(game, 1);
await game.phaseInterceptor.to(SelectModifierPhase, false); await game.phaseInterceptor.to(SelectModifierPhase, false);
@ -141,11 +143,12 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; 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[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT"); 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");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT");
}); });
it("should leave encounter without battle", async () => { 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", () => { it("should have the correct properties", () => {
const option = WeirdDreamEncounter.options[1]; const option = WeirdDreamEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); 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"); const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
const levelsPrior = scene.getParty().map(p => p.level); const levelsPrior = scene.getParty().map(p => p.level);
await runMysteryEncounterToEnd(game, 2); await runMysteryEncounterToEnd(game, 3);
const levelsAfter = scene.getParty().map(p => p.level); const levelsAfter = scene.getParty().map(p => p.level);
for (let i = 0; i < levelsPrior.length; i++) { 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); expect(scene.getParty()[i].levelExp).toBe(0);
} }
@ -195,7 +244,7 @@ describe("Weird Dream - Mystery Encounter", () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 2); await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled(); expect(leaveEncounterWithoutBattleSpy).toBeCalled();
}); });

View File

@ -1,12 +1,13 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Moves } from "#app/enums/moves"; import Overrides from "#app/overrides";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { Command } from "#app/ui/command-ui-handler"; import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui"; 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 { vi } from "vitest";
import { getMovePosition } from "../gameManagerUtils";
import { GameManagerHelper } from "./gameManagerHelper";
/** /**
* Helper to handle a Pokemon's move * 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`. * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`.
* Used to force a move to hit. * Used to force a move to hit.
*/ */
async forceHit(): Promise<void> { public async forceHit(): Promise<void> {
await this.game.phaseInterceptor.to(MoveEffectPhase, false); await this.game.phaseInterceptor.to(MoveEffectPhase, false);
vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); 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 * Intercepts {@linkcode MoveEffectPhase} and mocks the
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`. * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`.
* Used to force a move to miss. * 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); await this.game.phaseInterceptor.to(MoveEffectPhase, false);
const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck"); 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} * Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase}
* @param move the move to use * @param move - the move to use
* @param pkmIndex the pokemon index. Relevant for double-battles only (defaults to 0) * @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 * @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) { public select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) {
const movePosition = getMovePosition(this.game.scene, pkmIndex, move); const movePosition = getMovePosition(this.game.scene, pkmIndex, move);
this.game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { this.game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
@ -59,4 +60,15 @@ export class MoveHelper extends GameManagerHelper {
this.game.selectTarget(movePosition, targetIndex); 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 * @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 * @param biome the biome to set
*/ */
startingBiome(biome: Biome): this { public startingBiome(biome: Biome): this {
this.game.scene.newArena(biome); this.game.scene.newArena(biome);
this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`);
return this; return this;
@ -38,9 +38,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the starting wave (index) * Override the starting wave (index)
* @param wave the wave (index) to set. Classic: `1`-`200` * @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); vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave);
this.log(`Starting wave set to ${wave}!`); this.log(`Starting wave set to ${wave}!`);
return this; return this;
@ -49,9 +49,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) starting level * Override the player (pokemon) starting level
* @param level the (pokemon) level to set * @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); vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(level);
this.log(`Player Pokemon starting level set to ${level}!`); this.log(`Player Pokemon starting level set to ${level}!`);
return this; return this;
@ -62,7 +62,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param value the XP multiplier to set * @param value the XP multiplier to set
* @returns `this` * @returns `this`
*/ */
xpMultiplier(value: number): this { public xpMultiplier(value: number): this {
vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value); vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value);
this.log(`XP Multiplier set to ${value}!`); this.log(`XP Multiplier set to ${value}!`);
return this; return this;
@ -71,9 +71,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) starting held items * Override the player (pokemon) starting held items
* @param items the items to hold * @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); vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
this.log("Player Pokemon starting held items set to:", items); this.log("Player Pokemon starting held items set to:", items);
return this; return this;
@ -82,9 +82,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) {@linkcode Species | species} * Override the player (pokemon) {@linkcode Species | species}
* @param species the (pokemon) {@linkcode Species | species} to set * @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); vi.spyOn(Overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Player Pokemon species set to ${Species[species]} (=${species})!`); this.log(`Player Pokemon species set to ${Species[species]} (=${species})!`);
return this; return this;
@ -92,9 +92,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) to be a random fusion * 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); vi.spyOn(Overrides, "STARTER_FUSION_OVERRIDE", "get").mockReturnValue(true);
this.log("Player Pokemon is a random fusion!"); this.log("Player Pokemon is a random fusion!");
return this; return this;
@ -103,9 +103,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) fusion species * Override the player (pokemon) fusion species
* @param species the fusion species to set * @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); vi.spyOn(Overrides, "STARTER_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Player Pokemon fusion species set to ${Species[species]} (=${species})!`); this.log(`Player Pokemon fusion species set to ${Species[species]} (=${species})!`);
return this; return this;
@ -114,9 +114,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemons) forms * Override the player (pokemons) forms
* @param forms the (pokemon) forms to set * @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); vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue(forms);
const formsStr = Object.entries(forms) const formsStr = Object.entries(forms)
.map(([ speciesId, formIndex ]) => `${Species[speciesId]}=${formIndex}`) .map(([ speciesId, formIndex ]) => `${Species[speciesId]}=${formIndex}`)
@ -128,9 +128,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player's starting modifiers * Override the player's starting modifiers
* @param modifiers the modifiers to set * @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); vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers);
this.log(`Player starting modifiers set to: ${modifiers}`); this.log(`Player starting modifiers set to: ${modifiers}`);
return this; return this;
@ -139,9 +139,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) {@linkcode Abilities | ability} * Override the player (pokemon) {@linkcode Abilities | ability}
* @param ability the (pokemon) {@linkcode Abilities | ability} to set * @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); vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(ability);
this.log(`Player Pokemon ability set to ${Abilities[ability]} (=${ability})!`); this.log(`Player Pokemon ability set to ${Abilities[ability]} (=${ability})!`);
return this; return this;
@ -150,9 +150,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) **passive** {@linkcode Abilities | ability} * Override the player (pokemon) **passive** {@linkcode Abilities | ability}
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set * @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); vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
this.log(`Player Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`); this.log(`Player Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`);
return this; return this;
@ -161,9 +161,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player (pokemon) {@linkcode Moves | moves}set * Override the player (pokemon) {@linkcode Moves | moves}set
* @param moveset the {@linkcode Moves | moves}set to 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); vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
if (!Array.isArray(moveset)) { if (!Array.isArray(moveset)) {
moveset = [ moveset ]; moveset = [ moveset ];
@ -178,7 +178,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @param statusEffect the {@linkcode StatusEffect | status-effect} to set
* @returns * @returns
*/ */
statusEffect(statusEffect: StatusEffect): this { public statusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
this.log(`Player Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); this.log(`Player Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
return this; return this;
@ -186,9 +186,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override each wave to not have standard trainer battles * Override each wave to not have standard trainer battles
* @returns this * @returns `this`
*/ */
disableTrainerWaves(): this { public disableTrainerWaves(): this {
const realFn = getGameMode; const realFn = getGameMode;
vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => { vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => {
const mode = realFn(gameMode); const mode = realFn(gameMode);
@ -201,9 +201,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override each wave to not have critical hits * 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); vi.spyOn(Overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true);
this.log("Critical hits are disabled!"); this.log("Critical hits are disabled!");
return this; return this;
@ -212,9 +212,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the {@linkcode WeatherType | weather (type)} * Override the {@linkcode WeatherType | weather (type)}
* @param type {@linkcode WeatherType | weather type} to set * @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); vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type);
this.log(`Weather set to ${Weather[type]} (=${type})!`); this.log(`Weather set to ${Weather[type]} (=${type})!`);
return this; return this;
@ -223,9 +223,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the seed * Override the seed
* @param seed the seed to set * @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(() => { vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => {
this.game.scene.waveSeed = seed; this.game.scene.waveSeed = seed;
Phaser.Math.RND.sow([ seed ]); Phaser.Math.RND.sow([ seed ]);
@ -239,9 +239,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the battle type (single or double) * Override the battle type (single or double)
* @param battleType battle type to set * @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); vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType);
this.log(`Battle type set to ${battleType} only!`); this.log(`Battle type set to ${battleType} only!`);
return this; return this;
@ -250,9 +250,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) {@linkcode Species | species} * Override the enemy (pokemon) {@linkcode Species | species}
* @param species the (pokemon) {@linkcode Species | species} to set * @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); vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Enemy Pokemon species set to ${Species[species]} (=${species})!`); this.log(`Enemy Pokemon species set to ${Species[species]} (=${species})!`);
return this; return this;
@ -260,9 +260,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) to be a random fusion * 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); vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true);
this.log("Enemy Pokemon is a random fusion!"); this.log("Enemy Pokemon is a random fusion!");
return this; return this;
@ -271,9 +271,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) fusion species * Override the enemy (pokemon) fusion species
* @param species the fusion species to set * @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); vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
this.log(`Enemy Pokemon fusion species set to ${Species[species]} (=${species})!`); this.log(`Enemy Pokemon fusion species set to ${Species[species]} (=${species})!`);
return this; return this;
@ -282,9 +282,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) {@linkcode Abilities | ability} * Override the enemy (pokemon) {@linkcode Abilities | ability}
* @param ability the (pokemon) {@linkcode Abilities | ability} to set * @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); vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
this.log(`Enemy Pokemon ability set to ${Abilities[ability]} (=${ability})!`); this.log(`Enemy Pokemon ability set to ${Abilities[ability]} (=${ability})!`);
return this; return this;
@ -293,9 +293,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) **passive** {@linkcode Abilities | ability} * Override the enemy (pokemon) **passive** {@linkcode Abilities | ability}
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set * @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); vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
this.log(`Enemy Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`); this.log(`Enemy Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`);
return this; return this;
@ -304,9 +304,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) {@linkcode Moves | moves}set * Override the enemy (pokemon) {@linkcode Moves | moves}set
* @param moveset the {@linkcode Moves | moves}set to 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); vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
if (!Array.isArray(moveset)) { if (!Array.isArray(moveset)) {
moveset = [ moveset ]; moveset = [ moveset ];
@ -319,9 +319,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) level * Override the enemy (pokemon) level
* @param level the level to set * @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); vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level);
this.log(`Enemy Pokemon level set to ${level}!`); this.log(`Enemy Pokemon level set to ${level}!`);
return this; return this;
@ -332,7 +332,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @param statusEffect the {@linkcode StatusEffect | status-effect} to set
* @returns * @returns
*/ */
enemyStatusEffect(statusEffect: StatusEffect): this { public enemyStatusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
return this; return this;
@ -341,9 +341,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (pokemon) held items * Override the enemy (pokemon) held items
* @param items the items to hold * @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); vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
this.log("Enemy Pokemon held items set to:", items); this.log("Enemy Pokemon held items set to:", items);
return this; return this;
@ -354,7 +354,7 @@ export class OverridesHelper extends GameManagerHelper {
* @param unlockable The Unlockable(s) to enable. * @param unlockable The Unlockable(s) to enable.
* @returns `this` * @returns `this`
*/ */
enableUnlockable(unlockable: Unlockables[]) { public enableUnlockable(unlockable: Unlockables[]): this {
vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable); vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable);
this.log("Temporarily unlocked the following content: ", unlockable); this.log("Temporarily unlocked the following content: ", unlockable);
return this; return this;
@ -363,9 +363,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the items rolled at the end of a battle * Override the items rolled at the end of a battle
* @param items the items to be rolled * @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); vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items);
this.log("Item rewards set to:", items); this.log("Item rewards set to:", items);
return this; return this;
@ -375,8 +375,9 @@ export class OverridesHelper extends GameManagerHelper {
* Override player shininess * Override player shininess
* @param shininess - `true` or `false` to force the player's pokemon to be shiny or not shiny, * @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. * `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); vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess);
if (shininess === null) { if (shininess === null) {
this.log("Disabled player Pokemon shiny override!"); this.log("Disabled player Pokemon shiny override!");
@ -389,8 +390,9 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override player shiny variant * Override player shiny variant
* @param variant - The player's 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); vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant);
this.log(`Set player Pokemon's shiny variant to ${variant}!`); this.log(`Set player Pokemon's shiny variant to ${variant}!`);
return this; return this;
@ -420,23 +422,38 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the enemy (Pokemon) to have the given amount of health segments * Override the enemy (Pokemon) to have the given amount of health segments
* @param healthSegments the number of segments to give * @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 * - `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 * - `1`: the Pokemon will not be a boss
* 2+: the Pokemon will be a boss with the given number of health segments * - `2`+: the Pokemon will be a boss with the given number of health segments
* @returns this * @returns `this`
*/ */
enemyHealthSegments(healthSegments: number) { public enemyHealthSegments(healthSegments: number): this {
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments); vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
this.log("Enemy Pokemon health segments set to:", healthSegments); this.log("Enemy Pokemon health segments set to:", healthSegments);
return this; 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. * Override the encounter chance for a mystery encounter.
* @param percentage the encounter chance in % * @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 maxRate: number = 256; // 100%
const rate = maxRate * (percentage / 100); const rate = maxRate * (percentage / 100);
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); 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. * Override the encounter chance for a mystery encounter.
* @returns spy instance * @param tier - The {@linkcode MysteryEncounterTier} to encounter
* @param tier * @returns `this`
*/ */
mysteryEncounterTier(tier: MysteryEncounterTier) { public mysteryEncounterTier(tier: MysteryEncounterTier): this {
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier);
this.log(`Mystery encounter tier set to ${tier}!`); this.log(`Mystery encounter tier set to ${tier}!`);
return this; return this;
@ -457,10 +474,10 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the encounter that spawns for the scene * Override the encounter that spawns for the scene
* @param encounterType * @param encounterType - The {@linkcode MysteryEncounterType} of the encounter
* @returns spy instance * @returns `this`
*/ */
mysteryEncounter(encounterType: MysteryEncounterType) { public mysteryEncounter(encounterType: MysteryEncounterType): this {
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType);
this.log(`Mystery encounter override set to ${encounterType}!`); this.log(`Mystery encounter override set to ${encounterType}!`);
return this; return this;

View File

@ -2,37 +2,83 @@ import BattleScene from "#app/battle-scene";
import { ModalConfig } from "./modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler";
import { Mode } from "./ui"; import { Mode } from "./ui";
import * as Utils from "../utils"; 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 { Button } from "#app/enums/buttons";
import { TextStyle } from "./text";
export default class AdminUiHandler extends FormModalUiHandler { 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) { constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode); super(scene, mode);
} }
setup(): void { override getModalTitle(): string {
super.setup();
}
getModalTitle(config?: ModalConfig): string {
return "Admin panel"; return "Admin panel";
} }
getFields(config?: ModalConfig): string[] { override getWidth(): number {
return [ "Username", "Discord ID" ]; return this.adminMode === AdminMode.ADMIN ? 180 : 160;
} }
getWidth(config?: ModalConfig): number { override getMargin(): [number, number, number, number] {
return 160; return [ 0, 0, 0, 0 ];
} }
getMargin(config?: ModalConfig): [number, number, number, number] { override getButtonLabels(): string[] {
return [ 0, 0, 48, 0 ]; 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[] { override getInputFieldConfigs(): InputFieldConfig[] {
return [ "Link account", "Cancel" ]; 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 { processInput(button: Button): boolean {
@ -45,44 +91,281 @@ export default class AdminUiHandler extends FormModalUiHandler {
} }
show(args: any[]): boolean { 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)) { if (super.show(args)) {
const config = args[0] as ModalConfig; this.populateFields(this.adminMode, this.adminResult);
const originalSubmitAction = this.submitAction; const originalSubmitAction = this.submitAction;
this.submitAction = (_) => { this.submitAction = (_) => {
this.submitAction = originalSubmitAction; 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: []}); this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
const onFail = error => { if (this.adminMode === AdminMode.LINK) {
this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() })); this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord
this.scene.ui.playError(); .then(response => {
}; if (response.error) {
if (!this.inputs[0].text) { return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind
return onFail("Username is required"); } 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 false;
}; };
return true; return true;
} }
return false; 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 { clear(): void {
super.clear(); 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 { WindowVariant, addWindow } from "./ui-theme";
import InputText from "phaser3-rex-plugins/plugins/inputtext"; import InputText from "phaser3-rex-plugins/plugins/inputtext";
import * as Utils from "../utils"; import * as Utils from "../utils";
import i18next from "i18next";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
export interface FormModalConfig extends ModalConfig { export interface FormModalConfig extends ModalConfig {
@ -19,6 +18,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
protected errorMessage: Phaser.GameObjects.Text; protected errorMessage: Phaser.GameObjects.Text;
protected submitAction: Function | null; protected submitAction: Function | null;
protected tween: Phaser.Tweens.Tween; protected tween: Phaser.Tweens.Tween;
protected formLabels: Phaser.GameObjects.Text[];
constructor(scene: BattleScene, mode: Mode | null = null) { constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode); super(scene, mode);
@ -26,12 +26,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
this.editing = false; this.editing = false;
this.inputContainers = []; this.inputContainers = [];
this.inputs = []; 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 { 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 { getReadableErrorMessage(error: string): string {
@ -45,37 +51,50 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
setup(): void { setup(): void {
super.setup(); super.setup();
const fields = this.getFields(); const config = this.getInputFieldConfigs();
const hasTitle = !!this.getModalTitle(); const hasTitle = !!this.getModalTitle();
fields.forEach((field, f) => { if (config.length >= 1) {
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT); 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); const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
inputContainer.setVisible(false); inputContainer.setVisible(false);
const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN); 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 isPassword = config?.isPassword;
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 }); 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); input.setOrigin(0, 0);
inputContainer.add(inputBg); inputContainer.add(inputBg);
inputContainer.add(input); inputContainer.add(input);
this.modalContainer.add(inputContainer);
this.inputContainers.push(inputContainer); this.inputContainers.push(inputContainer);
this.modalContainer.add(inputContainer);
this.inputs.push(input); 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 { 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 { ModalConfig } from "./modal-ui-handler";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { Mode } from "./ui"; import { Mode } from "./ui";
@ -17,9 +17,9 @@ interface BuildInteractableImageOpts {
export default class LoginFormUiHandler extends FormModalUiHandler { export default class LoginFormUiHandler extends FormModalUiHandler {
private readonly ERR_USERNAME: string = "invalid username"; private readonly ERR_USERNAME: string = "invalid username";
private readonly ERR_PASSWORD: string = "invalid password"; private readonly ERR_PASSWORD: string = "invalid password";
private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist"; private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist";
private readonly ERR_PASSWORD_MATCH: string = "password doesn't match"; private readonly ERR_PASSWORD_MATCH: string = "password doesn't match";
private readonly ERR_NO_SAVES: string = "No save files found"; private readonly ERR_NO_SAVES: string = "No save files found";
private readonly ERR_TOO_MANY_SAVES: string = "Too many 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"); return i18next.t("menu:login");
} }
override getFields(_config?: ModalConfig): string[] {
return [ i18next.t("menu:username"), i18next.t("menu:password") ];
}
override getWidth(_config?: ModalConfig): number { override getWidth(_config?: ModalConfig): number {
return 160; return 160;
} }
@ -106,14 +102,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
case this.ERR_PASSWORD_MATCH: case this.ERR_PASSWORD_MATCH:
return i18next.t("menu:unmatchingPassword"); return i18next.t("menu:unmatchingPassword");
case this.ERR_NO_SAVES: case this.ERR_NO_SAVES:
return i18next.t("menu:noSaves"); return "P01: " + i18next.t("menu:noSaves");
case this.ERR_TOO_MANY_SAVES: case this.ERR_TOO_MANY_SAVES:
return i18next.t("menu:tooManySaves"); return "P02: " + i18next.t("menu:tooManySaves");
} }
return super.getReadableErrorMessage(error); 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 { override show(args: any[]): boolean {
if (super.show(args)) { 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")); [ 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.setText(i18next.t("menu:orUse") ?? "");
this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length); this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length);
this.externalPartyTitle.setVisible(true); this.externalPartyTitle.setVisible(true);

View File

@ -13,6 +13,7 @@ import { GameDataType } from "#enums/game-data-type";
import BgmBar from "#app/ui/bgm-bar"; import BgmBar from "#app/ui/bgm-bar";
import AwaitableUiHandler from "./awaitable-ui-handler"; import AwaitableUiHandler from "./awaitable-ui-handler";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { AdminMode, getAdminModeName } from "./admin-ui-handler";
enum MenuOptions { enum MenuOptions {
GAME_SETTINGS, GAME_SETTINGS,
@ -387,16 +388,41 @@ export default class MenuUiHandler extends MessageUiHandler {
communityOptions.push({ communityOptions.push({
label: "Admin", label: "Admin",
handler: () => { handler: () => {
ui.playSelect();
ui.setOverlayMode(Mode.ADMIN, { 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
buttonActions: [ 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
ui.revertMode(); options.push({
}, label: getAdminModeName(mode as AdminMode),
() => { handler: () => {
ui.revertMode(); 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; return true;
}, },

View File

@ -15,12 +15,14 @@ export abstract class ModalUiHandler extends UiHandler {
protected titleText: Phaser.GameObjects.Text; protected titleText: Phaser.GameObjects.Text;
protected buttonContainers: Phaser.GameObjects.Container[]; protected buttonContainers: Phaser.GameObjects.Container[];
protected buttonBgs: Phaser.GameObjects.NineSlice[]; protected buttonBgs: Phaser.GameObjects.NineSlice[];
protected buttonLabels: Phaser.GameObjects.Text[];
constructor(scene: BattleScene, mode: Mode | null = null) { constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode); super(scene, mode);
this.buttonContainers = []; this.buttonContainers = [];
this.buttonBgs = []; this.buttonBgs = [];
this.buttonLabels = [];
} }
abstract getModalTitle(config?: ModalConfig): string; abstract getModalTitle(config?: ModalConfig): string;
@ -75,6 +77,7 @@ export abstract class ModalUiHandler extends UiHandler {
const buttonContainer = this.scene.add.container(0, buttonTopMargin); const buttonContainer = this.scene.add.container(0, buttonTopMargin);
this.buttonLabels.push(buttonLabel);
this.buttonBgs.push(buttonBg); this.buttonBgs.push(buttonBg);
this.buttonContainers.push(buttonContainer); 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.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme));
this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, 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 // Check if the player owns ability for the root form
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs); const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(starterEntry.abilityAttr);
if (!playerOwnsThisAbility) { if (!playerOwnsThisAbility) {
this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme)); 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 { ModalConfig } from "./modal-ui-handler";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { Mode } from "./ui"; import { Mode } from "./ui";
@ -24,10 +24,6 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
return i18next.t("menu:register"); 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 { getWidth(config?: ModalConfig): number {
return 160; return 160;
} }
@ -61,6 +57,14 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
return super.getReadableErrorMessage(error); 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 { setup(): void {
super.setup(); 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 { ModalConfig } from "./modal-ui-handler";
import i18next from "i18next"; import i18next from "i18next";
import { PlayerPokemon } from "#app/field/pokemon"; import { PlayerPokemon } from "#app/field/pokemon";
@ -8,10 +8,6 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
return i18next.t("menu:renamePokemon"); return i18next.t("menu:renamePokemon");
} }
getFields(config?: ModalConfig): string[] {
return [ i18next.t("menu:nickname") ];
}
getWidth(config?: ModalConfig): number { getWidth(config?: ModalConfig): number {
return 160; return 160;
} }
@ -33,6 +29,10 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
return super.getReadableErrorMessage(error); return super.getReadableErrorMessage(error);
} }
override getInputFieldConfigs(): InputFieldConfig[] {
return [{ label: i18next.t("menu:nickname") }];
}
show(args: any[]): boolean { show(args: any[]): boolean {
if (super.show(args)) { if (super.show(args)) {
const config = args[0] as ModalConfig; 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 { ModalConfig } from "./modal-ui-handler";
import i18next from "i18next"; import i18next from "i18next";
import { PlayerPokemon } from "#app/field/pokemon"; import { PlayerPokemon } from "#app/field/pokemon";
@ -43,10 +43,6 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
return "Test Dialogue"; return "Test Dialogue";
} }
getFields(config?: ModalConfig): string[] {
return [ "Dialogue" ];
}
getWidth(config?: ModalConfig): number { getWidth(config?: ModalConfig): number {
return 300; return 300;
} }
@ -68,8 +64,15 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
return super.getReadableErrorMessage(error); return super.getReadableErrorMessage(error);
} }
override getInputFieldConfigs(): InputFieldConfig[] {
return [{ label: "Dialogue" }];
}
show(args: any[]): boolean { show(args: any[]): boolean {
const ui = this.getUi(); const ui = this.getUi();
const hasTitle = !!this.getModalTitle();
this.updateFields(this.getInputFieldConfigs(), hasTitle);
this.updateContainer(args[0] as ModalConfig);
const input = this.inputs[0]; const input = this.inputs[0];
input.setMaxLength(255); input.setMaxLength(255);