Merge beta and resolve conflicts
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 556 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -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$"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 664 B |
|
@ -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$"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 219 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 219 B |
|
@ -1 +1 @@
|
||||||
Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef
|
Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f
|
|
@ -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:
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
498
src/data/move.ts
|
@ -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)
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 ];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
];
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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, () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
|
@ -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());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|