mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-22 07:19:28 +02:00
Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into friendconsts
This commit is contained in:
commit
1658db4797
BIN
public/audio/cry/718-10-complete.m4a
Normal file
BIN
public/audio/cry/718-10-complete.m4a
Normal file
Binary file not shown.
@ -4,7 +4,7 @@ import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
|
||||
import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species";
|
||||
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
|
||||
import * as Utils from "#app/utils";
|
||||
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
||||
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
||||
import { PokeballType } from "#app/data/pokeball";
|
||||
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims";
|
||||
import { Phase } from "#app/phase";
|
||||
@ -790,7 +790,7 @@ export default class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
getEnemyParty(): EnemyPokemon[] {
|
||||
return this.currentBattle?.enemyParty || [];
|
||||
return this.currentBattle?.enemyParty ?? [];
|
||||
}
|
||||
|
||||
getEnemyPokemon(): EnemyPokemon | undefined {
|
||||
@ -1390,7 +1390,7 @@ export default class BattleScene extends SceneBase {
|
||||
case Species.GRENINJA:
|
||||
return Utils.randSeedInt(2);
|
||||
case Species.ZYGARDE:
|
||||
return Utils.randSeedInt(3);
|
||||
return Utils.randSeedInt(4);
|
||||
case Species.MINIOR:
|
||||
return Utils.randSeedInt(6);
|
||||
case Species.ALCREMIE:
|
||||
@ -2426,7 +2426,7 @@ export default class BattleScene extends SceneBase {
|
||||
return Math.floor(moneyValue / 10) * 10;
|
||||
}
|
||||
|
||||
addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean): Promise<boolean> {
|
||||
addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean, cost?: number): Promise<boolean> {
|
||||
if (!modifier) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
@ -2483,6 +2483,8 @@ export default class BattleScene extends SceneBase {
|
||||
}
|
||||
} else if (modifier instanceof FusePokemonModifier) {
|
||||
args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon);
|
||||
} else if (modifier instanceof RememberMoveModifier && !Utils.isNullOrUndefined(cost)) {
|
||||
args.push(cost);
|
||||
}
|
||||
|
||||
if (modifier.shouldApply(pokemon, ...args)) {
|
||||
|
@ -5535,16 +5535,18 @@ export function initAbilities() {
|
||||
.attr(UnsuppressableAbilityAbAttr)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.bypassFaint(),
|
||||
new Ability(Abilities.POWER_CONSTRUCT, 7) // TODO: 10% Power Construct Zygarde isn't accounted for yet. If changed, update Zygarde's getSpeciesFormIndex entry accordingly
|
||||
.attr(PostBattleInitFormChangeAbAttr, () => 2)
|
||||
.attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
|
||||
.attr(PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
|
||||
new Ability(Abilities.POWER_CONSTRUCT, 7)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostBattleInitFormChangeAbAttr, () => 2)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostBattleInitFormChangeAbAttr, () => 3)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(UnswappableAbilityAbAttr)
|
||||
.attr(UnsuppressableAbilityAbAttr)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.bypassFaint()
|
||||
.partial(),
|
||||
.bypassFaint(),
|
||||
new Ability(Abilities.CORROSION, 7)
|
||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ])
|
||||
.edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Arena } from "#app/field/arena";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { Type } from "#app/data/type";
|
||||
import * as Utils from "#app/utils";
|
||||
import { BooleanHolder, NumberHolder, toDmgValue } from "#app/utils";
|
||||
import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon";
|
||||
@ -36,7 +36,7 @@ export abstract class ArenaTag {
|
||||
public side: ArenaTagSide = ArenaTagSide.BOTH
|
||||
) {}
|
||||
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
apply(arena: Arena, simulated: boolean, ...args: unknown[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -122,10 +122,20 @@ export class MistTag extends ArenaTag {
|
||||
}
|
||||
}
|
||||
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
(args[0] as Utils.BooleanHolder).value = true;
|
||||
/**
|
||||
* Cancels the lowering of stats
|
||||
* @param arena the {@linkcode Arena} containing this effect
|
||||
* @param simulated `true` if the effect should be applied quietly
|
||||
* @param cancelled a {@linkcode BooleanHolder} whose value is set to `true`
|
||||
* to flag the stat reduction as cancelled
|
||||
* @returns `true` if a stat reduction was cancelled; `false` otherwise
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, cancelled: BooleanHolder): boolean {
|
||||
cancelled.value = true;
|
||||
|
||||
arena.scene.queueMessage(i18next.t("arenaTag:mistApply"));
|
||||
if (!simulated) {
|
||||
arena.scene.queueMessage(i18next.t("arenaTag:mistApply"));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -157,17 +167,15 @@ export class WeakenMoveScreenTag extends ArenaTag {
|
||||
/**
|
||||
* Applies the weakening effect to the move.
|
||||
*
|
||||
* @param arena - The arena where the move is applied.
|
||||
* @param args - The arguments for the move application.
|
||||
* @param args[0] - The category of the move.
|
||||
* @param args[1] - A boolean indicating whether it is a double battle.
|
||||
* @param args[2] - An object of type `Utils.NumberHolder` that holds the damage multiplier
|
||||
*
|
||||
* @returns True if the move was weakened, otherwise false.
|
||||
* @param arena the {@linkcode Arena} where the move is applied.
|
||||
* @param simulated n/a
|
||||
* @param moveCategory the attacking move's {@linkcode MoveCategory}.
|
||||
* @param damageMultiplier A {@linkcode NumberHolder} containing the damage multiplier
|
||||
* @returns `true` if the attacking move was weakened; `false` otherwise.
|
||||
*/
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
if (this.weakenedCategories.includes((args[0] as MoveCategory))) {
|
||||
(args[2] as Utils.NumberHolder).value = (args[1] as boolean) ? 2732 / 4096 : 0.5;
|
||||
override apply(arena: Arena, simulated: boolean, moveCategory: MoveCategory, damageMultiplier: NumberHolder): boolean {
|
||||
if (this.weakenedCategories.includes(moveCategory)) {
|
||||
damageMultiplier.value = arena.scene.currentBattle.double ? 2732 / 4096 : 0.5;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -249,38 +257,34 @@ export class ConditionalProtectTag extends ArenaTag {
|
||||
onRemove(arena: Arena): void { }
|
||||
|
||||
/**
|
||||
* apply(): Checks incoming moves against the condition function
|
||||
* Checks incoming moves against the condition function
|
||||
* and protects the target if conditions are met
|
||||
* @param arena The arena containing this tag
|
||||
* @param args\[0\] (Utils.BooleanHolder) Signals if the move is cancelled
|
||||
* @param args\[1\] (Pokemon) The Pokemon using the move
|
||||
* @param args\[2\] (Pokemon) The intended target of the move
|
||||
* @param args\[3\] (Moves) The parameters to the condition function
|
||||
* @param args\[4\] (Utils.BooleanHolder) Signals if the applied protection supercedes protection-ignoring effects
|
||||
* @returns
|
||||
* @param arena the {@linkcode Arena} containing this tag
|
||||
* @param simulated `true` if the tag is applied quietly; `false` otherwise.
|
||||
* @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against
|
||||
* @param attacker the attacking {@linkcode Pokemon}
|
||||
* @param defender the defending {@linkcode Pokemon}
|
||||
* @param moveId the {@linkcode Moves | identifier} for the move being used
|
||||
* @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection
|
||||
* @returns `true` if this tag protected against the attack; `false` otherwise
|
||||
*/
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
const [ cancelled, user, target, moveId, ignoresBypass ] = args;
|
||||
override apply(arena: Arena, simulated: boolean, isProtected: BooleanHolder, attacker: Pokemon, defender: Pokemon,
|
||||
moveId: Moves, ignoresProtectBypass: BooleanHolder): boolean {
|
||||
|
||||
if (cancelled instanceof Utils.BooleanHolder
|
||||
&& user instanceof Pokemon
|
||||
&& target instanceof Pokemon
|
||||
&& typeof moveId === "number"
|
||||
&& ignoresBypass instanceof Utils.BooleanHolder) {
|
||||
if ((this.side === ArenaTagSide.PLAYER) === defender.isPlayer()
|
||||
&& this.protectConditionFunc(arena, moveId)) {
|
||||
if (!isProtected.value) {
|
||||
isProtected.value = true;
|
||||
if (!simulated) {
|
||||
attacker.stopMultiHit(defender);
|
||||
|
||||
if ((this.side === ArenaTagSide.PLAYER) === target.isPlayer()
|
||||
&& this.protectConditionFunc(arena, moveId)) {
|
||||
if (!cancelled.value) {
|
||||
cancelled.value = true;
|
||||
user.stopMultiHit(target);
|
||||
|
||||
new CommonBattleAnim(CommonAnim.PROTECT, target).play(arena.scene);
|
||||
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
||||
new CommonBattleAnim(CommonAnim.PROTECT, defender).play(arena.scene);
|
||||
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(defender) }));
|
||||
}
|
||||
|
||||
ignoresBypass.value = ignoresBypass.value || this.ignoresBypass;
|
||||
return true;
|
||||
}
|
||||
|
||||
ignoresProtectBypass.value = ignoresProtectBypass.value || this.ignoresBypass;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -296,7 +300,7 @@ export class ConditionalProtectTag extends ArenaTag {
|
||||
*/
|
||||
const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => {
|
||||
const move = allMoves[moveId];
|
||||
const priority = new Utils.NumberHolder(move.priority);
|
||||
const priority = new NumberHolder(move.priority);
|
||||
const effectPhase = arena.scene.getCurrentPhase();
|
||||
|
||||
if (effectPhase instanceof MoveEffectPhase) {
|
||||
@ -460,7 +464,7 @@ class WishTag extends ArenaTag {
|
||||
if (user) {
|
||||
this.battlerIndex = user.getBattlerIndex();
|
||||
this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user) });
|
||||
this.healHp = Utils.toDmgValue(user.getMaxHp() / 2);
|
||||
this.healHp = toDmgValue(user.getMaxHp() / 2);
|
||||
} else {
|
||||
console.warn("Failed to get source for WishTag onAdd");
|
||||
}
|
||||
@ -497,12 +501,19 @@ export class WeakenMoveTypeTag extends ArenaTag {
|
||||
this.weakenedType = type;
|
||||
}
|
||||
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
if ((args[0] as Type) === this.weakenedType) {
|
||||
(args[1] as Utils.NumberHolder).value *= 0.33;
|
||||
/**
|
||||
* Reduces an attack's power by 0.33x if it matches this tag's weakened type.
|
||||
* @param arena n/a
|
||||
* @param simulated n/a
|
||||
* @param type the attack's {@linkcode Type}
|
||||
* @param power a {@linkcode NumberHolder} containing the attack's power
|
||||
* @returns `true` if the attack's power was reduced; `false` otherwise.
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, type: Type, power: NumberHolder): boolean {
|
||||
if (type === this.weakenedType) {
|
||||
power.value *= 0.33;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -563,13 +574,12 @@ export class IonDelugeTag extends ArenaTag {
|
||||
/**
|
||||
* Converts Normal-type moves to Electric type
|
||||
* @param arena n/a
|
||||
* @param args
|
||||
* - `[0]` {@linkcode Utils.NumberHolder} A container with a move's {@linkcode Type}
|
||||
* @param simulated n/a
|
||||
* @param moveType a {@linkcode NumberHolder} containing a move's {@linkcode Type}
|
||||
* @returns `true` if the given move type changed; `false` otherwise.
|
||||
*/
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
const moveType = args[0];
|
||||
if (moveType instanceof Utils.NumberHolder && moveType.value === Type.NORMAL) {
|
||||
override apply(arena: Arena, simulated: boolean, moveType: NumberHolder): boolean {
|
||||
if (moveType.value === Type.NORMAL) {
|
||||
moveType.value = Type.ELECTRIC;
|
||||
return true;
|
||||
}
|
||||
@ -608,16 +618,22 @@ export class ArenaTrapTag extends ArenaTag {
|
||||
}
|
||||
}
|
||||
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
const pokemon = args[0] as Pokemon;
|
||||
/**
|
||||
* Activates the hazard effect onto a Pokemon when it enters the field
|
||||
* @param arena the {@linkcode Arena} containing this tag
|
||||
* @param simulated if `true`, only checks if the hazard would activate.
|
||||
* @param pokemon the {@linkcode Pokemon} triggering this hazard
|
||||
* @returns `true` if this hazard affects the given Pokemon; `false` otherwise.
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||
if (this.sourceId === pokemon.id || (this.side === ArenaTagSide.PLAYER) !== pokemon.isPlayer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.activateTrap(pokemon);
|
||||
return this.activateTrap(pokemon, simulated);
|
||||
}
|
||||
|
||||
activateTrap(pokemon: Pokemon): boolean {
|
||||
activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -651,14 +667,18 @@ class SpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
}
|
||||
|
||||
activateTrap(pokemon: Pokemon): boolean {
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
|
||||
|
||||
if (simulated) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
if (!cancelled.value) {
|
||||
const damageHpRatio = 1 / (10 - 2 * this.layers);
|
||||
const damage = Utils.toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("arenaTag:spikesActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
pokemon.damageAndUpdate(damage, HitResult.OTHER);
|
||||
@ -702,8 +722,11 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
}
|
||||
|
||||
activateTrap(pokemon: Pokemon): boolean {
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
if (pokemon.isOfType(Type.POISON)) {
|
||||
this.neutralized = true;
|
||||
if (pokemon.scene.arena.removeTag(this.tagType)) {
|
||||
@ -807,8 +830,8 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
return damageHpRatio;
|
||||
}
|
||||
|
||||
activateTrap(pokemon: Pokemon): boolean {
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
|
||||
|
||||
if (cancelled.value) {
|
||||
@ -818,12 +841,16 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
|
||||
if (damageHpRatio) {
|
||||
const damage = Utils.toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
pokemon.scene.queueMessage(i18next.t("arenaTag:stealthRockActivateTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
pokemon.damageAndUpdate(damage, HitResult.OTHER);
|
||||
if (pokemon.turnData) {
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -853,14 +880,20 @@ class StickyWebTag extends ArenaTrapTag {
|
||||
}
|
||||
}
|
||||
|
||||
activateTrap(pokemon: Pokemon): boolean {
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
|
||||
|
||||
if (simulated) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
if (!cancelled.value) {
|
||||
pokemon.scene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
|
||||
const stages = new Utils.NumberHolder(-1);
|
||||
const stages = new NumberHolder(-1);
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -879,8 +912,15 @@ export class TrickRoomTag extends ArenaTag {
|
||||
super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId);
|
||||
}
|
||||
|
||||
apply(arena: Arena, args: any[]): boolean {
|
||||
const speedReversed = args[0] as Utils.BooleanHolder;
|
||||
/**
|
||||
* Reverses Speed-based turn order for all Pokemon on the field
|
||||
* @param arena n/a
|
||||
* @param simulated n/a
|
||||
* @param speedReversed a {@linkcode BooleanHolder} used to flag if Speed-based
|
||||
* turn order should be reversed.
|
||||
* @returns `true` if turn order is successfully reversed; `false` otherwise
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, speedReversed: BooleanHolder): boolean {
|
||||
speedReversed.value = !speedReversed.value;
|
||||
return true;
|
||||
}
|
||||
@ -1087,7 +1127,7 @@ class FireGrassPledgeTag extends ArenaTag {
|
||||
pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
// TODO: Replace this with a proper animation
|
||||
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM));
|
||||
pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / 8));
|
||||
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8));
|
||||
});
|
||||
|
||||
return super.lapse(arena);
|
||||
@ -1111,8 +1151,15 @@ class WaterFirePledgeTag extends ArenaTag {
|
||||
arena.scene.queueMessage(i18next.t(`arenaTag:waterFirePledgeOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`));
|
||||
}
|
||||
|
||||
override apply(arena: Arena, args: any[]): boolean {
|
||||
const moveChance = args[0] as Utils.NumberHolder;
|
||||
/**
|
||||
* Doubles the chance for the given move's secondary effect(s) to trigger
|
||||
* @param arena the {@linkcode Arena} containing this tag
|
||||
* @param simulated n/a
|
||||
* @param moveChance a {@linkcode NumberHolder} containing
|
||||
* the move's current effect chance
|
||||
* @returns `true` if the move's effect chance was doubled (currently always `true`)
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, moveChance: NumberHolder): boolean {
|
||||
moveChance.value *= 2;
|
||||
return true;
|
||||
}
|
||||
|
@ -2724,6 +2724,44 @@ export class TelekinesisTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag that swaps the user's base ATK stat with its base DEF stat.
|
||||
* @extends BattlerTag
|
||||
*/
|
||||
export class PowerTrickTag extends BattlerTag {
|
||||
constructor(sourceMove: Moves, sourceId: number) {
|
||||
super(BattlerTagType.POWER_TRICK, BattlerTagLapseType.CUSTOM, 0, sourceMove, sourceId, true);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
this.swapStat(pokemon);
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:powerTrickActive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
|
||||
onRemove(pokemon: Pokemon): void {
|
||||
this.swapStat(pokemon);
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:powerTrickActive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the Power Trick tag and reverts any stat changes if the tag is already applied.
|
||||
* @param {Pokemon} pokemon The {@linkcode Pokemon} that already has the Power Trick tag.
|
||||
*/
|
||||
onOverlap(pokemon: Pokemon): void {
|
||||
pokemon.removeTag(this.tagType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the user's base ATK stat with its base DEF stat.
|
||||
* @param {Pokemon} pokemon The {@linkcode Pokemon} whose stats will be swapped.
|
||||
*/
|
||||
swapStat(pokemon: Pokemon): void {
|
||||
const temp = pokemon.getStat(Stat.ATK, false);
|
||||
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.DEF, false), false);
|
||||
pokemon.setStat(Stat.DEF, temp, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||
* @param sourceId - The ID of the pokemon adding the tag
|
||||
@ -2899,6 +2937,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
return new SyrupBombTag(sourceId);
|
||||
case BattlerTagType.TELEKINESIS:
|
||||
return new TelekinesisTag(sourceMove);
|
||||
case BattlerTagType.POWER_TRICK:
|
||||
return new PowerTrickTag(sourceMove, sourceId);
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
194
src/data/move.ts
194
src/data/move.ts
@ -808,7 +808,7 @@ export default class Move implements Localizable {
|
||||
source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power);
|
||||
|
||||
if (!this.hasAttr(TypelessAttr)) {
|
||||
source.scene.arena.applyTags(WeakenMoveTypeTag, this.type, power);
|
||||
source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power);
|
||||
source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power);
|
||||
}
|
||||
|
||||
@ -1024,9 +1024,9 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
|
||||
|
||||
if (!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) {
|
||||
if ((!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) && !move.hasAttr(SecretPowerAttr)) {
|
||||
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, moveChance);
|
||||
user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance);
|
||||
}
|
||||
|
||||
if (!selfEffect) {
|
||||
@ -2875,6 +2875,162 @@ export class StatStageChangeAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to determine the Biome/Terrain-based secondary effect of Secret Power
|
||||
*/
|
||||
export class SecretPowerAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance
|
||||
* @returns `true` if the move's secondary effect should apply
|
||||
*/
|
||||
override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||
this.effectChanceOverride = move.chance;
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
|
||||
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
|
||||
// effectChanceOverride used in the application of the actual secondary effect
|
||||
this.effectChanceOverride = 100;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to apply the secondary effect to the target Pokemon
|
||||
* @returns `true` if a secondary effect is successfully applied
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
let secondaryEffect: MoveEffectAttr;
|
||||
const terrain = user.scene.arena.getTerrainType();
|
||||
if (terrain !== TerrainType.NONE) {
|
||||
secondaryEffect = this.determineTerrainEffect(terrain);
|
||||
} else {
|
||||
const biome = user.scene.arena.biomeType;
|
||||
secondaryEffect = this.determineBiomeEffect(biome);
|
||||
}
|
||||
return secondaryEffect.apply(user, target, move, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the secondary effect based on terrain.
|
||||
* Takes precedence over biome-based effects.
|
||||
* ```
|
||||
* Electric Terrain | Paralysis
|
||||
* Misty Terrain | SpAtk -1
|
||||
* Grassy Terrain | Sleep
|
||||
* Psychic Terrain | Speed -1
|
||||
* ```
|
||||
* @param terrain - {@linkcode TerrainType} The current terrain
|
||||
* @returns the chosen secondary effect {@linkcode MoveEffectAttr}
|
||||
*/
|
||||
private determineTerrainEffect(terrain: TerrainType): MoveEffectAttr {
|
||||
let secondaryEffect: MoveEffectAttr;
|
||||
switch (terrain) {
|
||||
case TerrainType.ELECTRIC:
|
||||
default:
|
||||
secondaryEffect = new StatusEffectAttr(StatusEffect.PARALYSIS, false);
|
||||
break;
|
||||
case TerrainType.MISTY:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.SPATK ], -1, false);
|
||||
break;
|
||||
case TerrainType.GRASSY:
|
||||
secondaryEffect = new StatusEffectAttr(StatusEffect.SLEEP, false);
|
||||
break;
|
||||
case TerrainType.PSYCHIC:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.SPD ], -1, false);
|
||||
break;
|
||||
}
|
||||
return secondaryEffect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the secondary effect based on biome
|
||||
* ```
|
||||
* Town, Metropolis, Slum, Dojo, Laboratory, Power Plant + Default | Paralysis
|
||||
* Plains, Grass, Tall Grass, Forest, Jungle, Meadow | Sleep
|
||||
* Swamp, Mountain, Temple, Ruins | Speed -1
|
||||
* Ice Cave, Snowy Forest | Freeze
|
||||
* Volcano | Burn
|
||||
* Fairy Cave | SpAtk -1
|
||||
* Desert, Construction Site, Beach, Island, Badlands | Accuracy -1
|
||||
* Sea, Lake, Seabed | Atk -1
|
||||
* Cave, Wasteland, Graveyard, Abyss, Space | Flinch
|
||||
* End | Def -1
|
||||
* ```
|
||||
* @param biome - The current {@linkcode Biome} the battle is set in
|
||||
* @returns the chosen secondary effect {@linkcode MoveEffectAttr}
|
||||
*/
|
||||
private determineBiomeEffect(biome: Biome): MoveEffectAttr {
|
||||
let secondaryEffect: MoveEffectAttr;
|
||||
switch (biome) {
|
||||
case Biome.PLAINS:
|
||||
case Biome.GRASS:
|
||||
case Biome.TALL_GRASS:
|
||||
case Biome.FOREST:
|
||||
case Biome.JUNGLE:
|
||||
case Biome.MEADOW:
|
||||
secondaryEffect = new StatusEffectAttr(StatusEffect.SLEEP, false);
|
||||
break;
|
||||
case Biome.SWAMP:
|
||||
case Biome.MOUNTAIN:
|
||||
case Biome.TEMPLE:
|
||||
case Biome.RUINS:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.SPD ], -1, false);
|
||||
break;
|
||||
case Biome.ICE_CAVE:
|
||||
case Biome.SNOWY_FOREST:
|
||||
secondaryEffect = new StatusEffectAttr(StatusEffect.FREEZE, false);
|
||||
break;
|
||||
case Biome.VOLCANO:
|
||||
secondaryEffect = new StatusEffectAttr(StatusEffect.BURN, false);
|
||||
break;
|
||||
case Biome.FAIRY_CAVE:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.SPATK ], -1, false);
|
||||
break;
|
||||
case Biome.DESERT:
|
||||
case Biome.CONSTRUCTION_SITE:
|
||||
case Biome.BEACH:
|
||||
case Biome.ISLAND:
|
||||
case Biome.BADLANDS:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.ACC ], -1, false);
|
||||
break;
|
||||
case Biome.SEA:
|
||||
case Biome.LAKE:
|
||||
case Biome.SEABED:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.ATK ], -1, false);
|
||||
break;
|
||||
case Biome.CAVE:
|
||||
case Biome.WASTELAND:
|
||||
case Biome.GRAVEYARD:
|
||||
case Biome.ABYSS:
|
||||
case Biome.SPACE:
|
||||
secondaryEffect = new AddBattlerTagAttr(BattlerTagType.FLINCHED, false, true);
|
||||
break;
|
||||
case Biome.END:
|
||||
secondaryEffect = new StatStageChangeAttr([ Stat.DEF ], -1, false);
|
||||
break;
|
||||
case Biome.TOWN:
|
||||
case Biome.METROPOLIS:
|
||||
case Biome.SLUM:
|
||||
case Biome.DOJO:
|
||||
case Biome.FACTORY:
|
||||
case Biome.LABORATORY:
|
||||
case Biome.POWER_PLANT:
|
||||
default:
|
||||
secondaryEffect = new StatusEffectAttr(StatusEffect.PARALYSIS, false);
|
||||
break;
|
||||
}
|
||||
return secondaryEffect;
|
||||
}
|
||||
}
|
||||
|
||||
export class PostVictoryStatStageChangeAttr extends MoveAttr {
|
||||
private stats: BattleStat[];
|
||||
private stages: number;
|
||||
@ -3834,8 +3990,8 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
||||
|
||||
for (const p of pokemonActed) {
|
||||
const [ lastMove ] = p.getLastXMoves(1);
|
||||
if (lastMove.result !== MoveResult.FAIL) {
|
||||
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
|
||||
if (lastMove?.result !== MoveResult.FAIL) {
|
||||
if ((lastMove?.result === MoveResult.SUCCESS) && (lastMove?.move === this.move)) {
|
||||
power.value *= 2;
|
||||
return true;
|
||||
} else {
|
||||
@ -4736,7 +4892,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0].result === MoveResult.FAIL)) {
|
||||
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0]?.result === MoveResult.FAIL)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@ -5174,7 +5330,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) {
|
||||
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
||||
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY);
|
||||
return true;
|
||||
}
|
||||
@ -5249,7 +5405,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
|
||||
if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) {
|
||||
if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
|
||||
user.scene.arena.addTag(this.tagType, 0, move.id, user.id, side);
|
||||
if (!tag) {
|
||||
return true;
|
||||
@ -5386,7 +5542,7 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr {
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// TODO: add support for `HIT` effect triggering in AddArenaTagAttr to remove the need for this check
|
||||
if (user.getLastXMoves(1)[0].result !== MoveResult.SUCCESS) {
|
||||
if (user.getLastXMoves(1)[0]?.result !== MoveResult.SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -5484,37 +5640,38 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
*/
|
||||
const switchOutTarget = this.selfSwitch ? user : target;
|
||||
if (switchOutTarget instanceof PlayerPokemon) {
|
||||
// Switch out logic for the player's Pokemon
|
||||
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
return false;
|
||||
}
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
|
||||
// Switch out logic for trainer battles
|
||||
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
return false;
|
||||
}
|
||||
// Switch out logic for trainer battles
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
// for opponent switching out
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(),
|
||||
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
|
||||
false, false), MoveEndPhase);
|
||||
}
|
||||
} else {
|
||||
// Switch out logic for everything else (eg: WILD battles)
|
||||
if (user.scene.currentBattle.waveIndex % 10 === 0) {
|
||||
return false;
|
||||
}
|
||||
// Switch out logic for everything else (eg: WILD battles)
|
||||
switchOutTarget.leaveField(false);
|
||||
|
||||
if (switchOutTarget.hp) {
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(false);
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||
|
||||
// in double battles redirect potential moves off fled pokemon
|
||||
@ -6429,6 +6586,9 @@ export class TransformAttr extends MoveEffectAttr {
|
||||
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);
|
||||
@ -7894,7 +8054,7 @@ export function initMoves() {
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3)
|
||||
.makesContact(false)
|
||||
.partial(), // No effect implemented
|
||||
.attr(SecretPowerAttr),
|
||||
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
|
||||
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true)
|
||||
.attr(GulpMissileTagAttr)
|
||||
@ -8152,7 +8312,7 @@ export function initMoves() {
|
||||
.attr(OpponentHighHpPowerAttr, 120)
|
||||
.makesContact(),
|
||||
new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4)
|
||||
.unimplemented(),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true),
|
||||
new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4)
|
||||
.attr(SuppressAbilitiesAttr),
|
||||
new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4)
|
||||
|
@ -154,7 +154,7 @@ export const ATrainersTestEncounter: MysteryEncounter =
|
||||
};
|
||||
encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`));
|
||||
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.SACRED_ASH ], guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ULTRA ], fillRemaining: true }, [ eggOptions ]);
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
}
|
||||
)
|
||||
.withSimpleOption(
|
||||
|
@ -286,7 +286,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
|
||||
ignorePp: true
|
||||
});
|
||||
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
|
||||
})
|
||||
.build()
|
||||
@ -328,7 +328,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
|
||||
});
|
||||
await scene.updateModifiers(true);
|
||||
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
.build()
|
||||
@ -359,7 +359,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
|
||||
greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ];
|
||||
greedent.passive = true;
|
||||
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false);
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
|
@ -228,7 +228,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Learn its Dance
|
||||
hideOricorioPokemon(scene);
|
||||
await hideOricorioPokemon(scene);
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
.build()
|
||||
@ -303,7 +303,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
|
||||
}
|
||||
}
|
||||
|
||||
hideOricorioPokemon(scene);
|
||||
await hideOricorioPokemon(scene);
|
||||
await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false);
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
|
@ -182,7 +182,7 @@ export const DarkDealEncounter: MysteryEncounter =
|
||||
const config: EnemyPartyConfig = {
|
||||
pokemonConfigs: [ pokemonConfig ],
|
||||
};
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
|
@ -222,12 +222,13 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
],
|
||||
})
|
||||
.withPreOptionPhase(async (scene: BattleScene) => {
|
||||
// Do NOT await this, to prevent player from repeatedly pressing options
|
||||
transitionMysteryEncounterIntroVisuals(scene, false, false, 2000);
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Fire types help calm the Volcarona
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
transitionMysteryEncounterIntroVisuals(scene);
|
||||
await transitionMysteryEncounterIntroVisuals(scene);
|
||||
setEncounterRewards(scene,
|
||||
{ fillRemaining: true },
|
||||
undefined,
|
||||
|
@ -152,7 +152,7 @@ export const FunAndGamesEncounter: MysteryEncounter =
|
||||
},
|
||||
async (scene: BattleScene) => {
|
||||
// Leave encounter with no rewards or exp
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true);
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
return true;
|
||||
}
|
||||
|
@ -399,7 +399,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
|
||||
if (modifier.stackCount === 0) {
|
||||
scene.removeModifier(modifier);
|
||||
}
|
||||
scene.updateModifiers(true, true);
|
||||
await scene.updateModifiers(true, true);
|
||||
|
||||
// Generate a trainer name
|
||||
const traderName = generateRandomTraderName();
|
||||
|
@ -129,7 +129,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
|
||||
*
|
||||
* @param scene Battle scene
|
||||
*/
|
||||
async function handlePokemonGuidingYouPhase(scene: BattleScene) {
|
||||
function handlePokemonGuidingYouPhase(scene: BattleScene) {
|
||||
const laprasSpecies = getPokemonSpecies(Species.LAPRAS);
|
||||
const { mysteryEncounter } = scene.currentBattle;
|
||||
|
||||
|
@ -147,11 +147,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
|
||||
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM ], fillRemaining: true });
|
||||
|
||||
// Seed offsets to remove possibility of different trainers having exact same teams
|
||||
let ret;
|
||||
let initBattlePromise: Promise<void>;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
ret = initBattleWithEnemyConfig(scene, config);
|
||||
initBattlePromise = initBattleWithEnemyConfig(scene, config);
|
||||
}, scene.currentBattle.waveIndex * 10);
|
||||
return ret;
|
||||
await initBattlePromise!;
|
||||
}
|
||||
)
|
||||
.withSimpleOption(
|
||||
@ -172,11 +172,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
|
||||
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: true });
|
||||
|
||||
// Seed offsets to remove possibility of different trainers having exact same teams
|
||||
let ret;
|
||||
let initBattlePromise: Promise<void>;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
ret = initBattleWithEnemyConfig(scene, config);
|
||||
initBattlePromise = initBattleWithEnemyConfig(scene, config);
|
||||
}, scene.currentBattle.waveIndex * 100);
|
||||
return ret;
|
||||
await initBattlePromise!;
|
||||
}
|
||||
)
|
||||
.withSimpleOption(
|
||||
@ -200,11 +200,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
|
||||
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT ], fillRemaining: true });
|
||||
|
||||
// Seed offsets to remove possibility of different trainers having exact same teams
|
||||
let ret;
|
||||
let initBattlePromise: Promise<void>;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
ret = initBattleWithEnemyConfig(scene, config);
|
||||
initBattlePromise = initBattleWithEnemyConfig(scene, config);
|
||||
}, scene.currentBattle.waveIndex * 1000);
|
||||
return ret;
|
||||
await initBattlePromise!;
|
||||
}
|
||||
)
|
||||
.withOutroDialogue([
|
||||
|
@ -184,7 +184,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
|
||||
scene.unshiftPhase(new GameOverPhase(scene));
|
||||
} else {
|
||||
// Show which Pokemon was KOed, then start battle against Gimmighoul
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
setEncounterRewards(scene, { fillRemaining: true });
|
||||
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
|
||||
}
|
||||
|
@ -303,13 +303,11 @@ async function summonSafariPokemon(scene: BattleScene) {
|
||||
scene.unshiftPhase(new SummonPhase(scene, 0, false));
|
||||
|
||||
encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon));
|
||||
showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", null, 1500, false)
|
||||
.then(() => {
|
||||
const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier);
|
||||
if (ivScannerModifier) {
|
||||
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)));
|
||||
}
|
||||
});
|
||||
|
||||
const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier);
|
||||
if (ivScannerModifier) {
|
||||
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)));
|
||||
}
|
||||
}
|
||||
|
||||
function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> {
|
||||
|
@ -142,7 +142,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
|
||||
encounter.setDialogueToken("newNature", getNatureName(newNature));
|
||||
queueEncounterMessage(scene, `${namespace}:cheap_side_effects`);
|
||||
setEncounterExp(scene, [ chosenPokemon.id ], 100);
|
||||
chosenPokemon.updateInfo();
|
||||
await chosenPokemon.updateInfo();
|
||||
})
|
||||
.build()
|
||||
)
|
||||
@ -204,7 +204,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
|
||||
queueEncounterMessage(scene, `${namespace}:no_bad_effects`);
|
||||
setEncounterExp(scene, [ chosenPokemon.id ], 100);
|
||||
|
||||
chosenPokemon.updateInfo();
|
||||
await chosenPokemon.updateInfo();
|
||||
})
|
||||
.build()
|
||||
)
|
||||
|
@ -149,7 +149,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
|
||||
const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.STEEL ])!;
|
||||
const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.ELECTRIC ])!;
|
||||
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ magnet, metalCoat ], fillRemaining: true });
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
}
|
||||
)
|
||||
|
@ -245,7 +245,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
|
||||
}
|
||||
|
||||
encounter.onGameOver = onGameOver;
|
||||
initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.withPostOptionPhase(async (scene: BattleScene) => {
|
||||
await doPostEncounterCleanup(scene);
|
||||
@ -297,7 +297,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
|
||||
}
|
||||
|
||||
encounter.onGameOver = onGameOver;
|
||||
initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.withPostOptionPhase(async (scene: BattleScene) => {
|
||||
await doPostEncounterCleanup(scene);
|
||||
@ -349,7 +349,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
|
||||
}
|
||||
|
||||
encounter.onGameOver = onGameOver;
|
||||
initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.withPostOptionPhase(async (scene: BattleScene) => {
|
||||
await doPostEncounterCleanup(scene);
|
||||
|
@ -201,7 +201,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
|
||||
});
|
||||
|
||||
encounter.dialogue.outro = [];
|
||||
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
|
||||
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
|
||||
}
|
||||
)
|
||||
|
@ -111,8 +111,8 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter =
|
||||
},
|
||||
async (scene: BattleScene) => {
|
||||
// Spawn 5 trainer battles back to back with Macho Brace in rewards
|
||||
scene.currentBattle.mysteryEncounter!.doContinueEncounter = (scene: BattleScene) => {
|
||||
return endTrainerBattleAndShowDialogue(scene);
|
||||
scene.currentBattle.mysteryEncounter!.doContinueEncounter = async (scene: BattleScene) => {
|
||||
await endTrainerBattleAndShowDialogue(scene);
|
||||
};
|
||||
await transitionMysteryEncounterIntroVisuals(scene, true, false);
|
||||
await spawnNextTrainerOrEndEncounter(scene);
|
||||
|
@ -162,7 +162,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
|
||||
|
||||
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
|
||||
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
@ -238,7 +238,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
|
||||
|
||||
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
|
||||
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
@ -351,7 +351,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
|
||||
|
||||
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
|
||||
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
|
@ -105,7 +105,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Gain 2 Leftovers and 2 Shell Bell
|
||||
transitionMysteryEncounterIntroVisuals(scene);
|
||||
await transitionMysteryEncounterIntroVisuals(scene);
|
||||
await tryApplyDigRewardItems(scene);
|
||||
|
||||
const blackSludge = generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ SHOP_ITEM_COST_MULTIPLIER ]);
|
||||
@ -136,7 +136,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
|
||||
// Investigate garbage, battle Gmax Garbodor
|
||||
scene.setFieldScale(0.75);
|
||||
await showEncounterText(scene, `${namespace}:option.2.selected_2`);
|
||||
transitionMysteryEncounterIntroVisuals(scene);
|
||||
await transitionMysteryEncounterIntroVisuals(scene);
|
||||
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
|
||||
@ -222,7 +222,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) {
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGainCount", { modifierName: shellBell.name, count: 2 }), null, undefined, true);
|
||||
}
|
||||
|
||||
async function doGarbageDig(scene: BattleScene) {
|
||||
function doGarbageDig(scene: BattleScene) {
|
||||
scene.playSound("battle_anims/PRSFX- Dig2");
|
||||
scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => {
|
||||
scene.playSound("battle_anims/PRSFX- Dig2");
|
||||
|
@ -799,8 +799,8 @@ export const pokemonFormChanges: PokemonFormChanges = {
|
||||
[Species.ZYGARDE]: [
|
||||
new SpeciesFormChange(Species.ZYGARDE, "50-pc", "complete", new SpeciesFormChangeManualTrigger(), true),
|
||||
new SpeciesFormChange(Species.ZYGARDE, "complete", "50-pc", new SpeciesFormChangeManualTrigger(), true),
|
||||
new SpeciesFormChange(Species.ZYGARDE, "10-pc", "complete", new SpeciesFormChangeManualTrigger(), true),
|
||||
new SpeciesFormChange(Species.ZYGARDE, "complete", "10-pc", new SpeciesFormChangeManualTrigger(), true)
|
||||
new SpeciesFormChange(Species.ZYGARDE, "10-pc", "10-complete", new SpeciesFormChangeManualTrigger(), true),
|
||||
new SpeciesFormChange(Species.ZYGARDE, "10-complete", "10-pc", new SpeciesFormChangeManualTrigger(), true)
|
||||
],
|
||||
[Species.DIANCIE]: [
|
||||
new SpeciesFormChange(Species.DIANCIE, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.DIANCITE))
|
||||
|
@ -425,6 +425,7 @@ export abstract class PokemonSpeciesForm {
|
||||
case "hero":
|
||||
case "roaming":
|
||||
case "complete":
|
||||
case "10-complete":
|
||||
case "10":
|
||||
case "10-pc":
|
||||
case "super":
|
||||
@ -2135,7 +2136,8 @@ export function initSpecies() {
|
||||
new PokemonForm("10% Forme", "10", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.AURA_BREAK, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, null, true),
|
||||
new PokemonForm("50% Forme Power Construct", "50-pc", Type.DRAGON, Type.GROUND, 5, 305, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 600, 108, 100, 121, 81, 95, 95, 3, 0, 300, false, "", true),
|
||||
new PokemonForm("10% Forme Power Construct", "10-pc", Type.DRAGON, Type.GROUND, 1.2, 33.5, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 486, 54, 100, 71, 61, 85, 115, 3, 0, 300, false, "10", true),
|
||||
new PokemonForm("Complete Forme", "complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300),
|
||||
new PokemonForm("Complete Forme (50% PC)", "complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300),
|
||||
new PokemonForm("Complete Forme (10% PC)", "10-complete", Type.DRAGON, Type.GROUND, 4.5, 610, Abilities.POWER_CONSTRUCT, Abilities.NONE, Abilities.NONE, 708, 216, 100, 121, 91, 95, 85, 3, 0, 300, false, "complete"),
|
||||
),
|
||||
new PokemonSpecies(Species.DIANCIE, 6, false, false, true, "Jewel Pokémon", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, GrowthRate.SLOW, null, false, true,
|
||||
new PokemonForm("Normal", "", Type.ROCK, Type.FAIRY, 0.7, 8.8, Abilities.CLEAR_BODY, Abilities.NONE, Abilities.NONE, 600, 50, 100, 150, 100, 150, 50, 3, 50, 300, false, null, true),
|
||||
|
@ -574,13 +574,13 @@ export class TrainerConfig {
|
||||
case "magma": {
|
||||
return {
|
||||
[TrainerPoolTier.COMMON]: [ Species.GROWLITHE, Species.SLUGMA, Species.SOLROCK, Species.HIPPOPOTAS, Species.BALTOY, Species.ROLYCOLY, Species.GLIGAR, Species.TORKOAL, Species.HOUNDOUR, Species.MAGBY ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.TRAPINCH, Species.SILICOBRA, Species.RHYHORN, Species.ANORITH, Species.LILEEP, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.BARBOACH ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.TRAPINCH, Species.SILICOBRA, Species.RHYHORN, Species.ANORITH, Species.LILEEP, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.TOEDSCOOL ],
|
||||
[TrainerPoolTier.RARE]: [ Species.CAPSAKID, Species.CHARCADET ]
|
||||
};
|
||||
}
|
||||
case "aqua": {
|
||||
return {
|
||||
[TrainerPoolTier.COMMON]: [ Species.CORPHISH, Species.SPHEAL, Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.LOTAD, Species.WAILMER, Species.REMORAID ],
|
||||
[TrainerPoolTier.COMMON]: [ Species.CORPHISH, Species.SPHEAL, Species.CLAMPERL, Species.CHINCHOU, Species.WOOPER, Species.WINGULL, Species.TENTACOOL, Species.AZURILL, Species.LOTAD, Species.WAILMER, Species.REMORAID, Species.BARBOACH ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.MANTYKE, Species.HISUI_QWILFISH, Species.ARROKUDA, Species.DHELMISE, Species.CLOBBOPUS, Species.FEEBAS, Species.PALDEA_WOOPER, Species.HORSEA, Species.SKRELP ],
|
||||
[TrainerPoolTier.RARE]: [ Species.DONDOZO, Species.BASCULEGION ]
|
||||
};
|
||||
@ -601,9 +601,9 @@ export class TrainerConfig {
|
||||
}
|
||||
case "flare": {
|
||||
return {
|
||||
[TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY ],
|
||||
[TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.INKAY, Species.FOONGUS, Species.HELIOPTILE, Species.ELECTRIKE, Species.SKORUPI, Species.PURRLOIN, Species.CLAWITZER, Species.PANCHAM, Species.ESPURR, Species.BUNNELBY ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.LITWICK, Species.SNEASEL, Species.PUMPKABOO, Species.PHANTUMP, Species.HONEDGE, Species.BINACLE, Species.HOUNDOUR, Species.SKRELP, Species.SLIGGOO ],
|
||||
[TrainerPoolTier.RARE]: [ Species.NOIVERN, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO ]
|
||||
[TrainerPoolTier.RARE]: [ Species.NOIBAT, Species.HISUI_AVALUGG, Species.HISUI_SLIGGOO ]
|
||||
};
|
||||
}
|
||||
case "aether": {
|
||||
@ -1504,7 +1504,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setSpeciesPools({
|
||||
[TrainerPoolTier.COMMON]: [ Species.SLUGMA, Species.POOCHYENA, Species.NUMEL, Species.ZIGZAGOON, Species.DIGLETT, Species.MAGBY, Species.TORKOAL, Species.GROWLITHE, Species.BALTOY ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.SOLROCK, Species.HIPPOPOTAS, Species.SANDACONDA, Species.PHANPY, Species.ROLYCOLY, Species.GLIGAR, Species.RHYHORN, Species.HEATMOR ],
|
||||
[TrainerPoolTier.RARE]: [ Species.TRAPINCH, Species.LILEEP, Species.ANORITH, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON ],
|
||||
[TrainerPoolTier.RARE]: [ Species.TRAPINCH, Species.LILEEP, Species.ANORITH, Species.HISUI_GROWLITHE, Species.TURTONATOR, Species.ARON, Species.TOEDSCOOL ],
|
||||
[TrainerPoolTier.SUPER_RARE]: [ Species.CAPSAKID, Species.CHARCADET ]
|
||||
}),
|
||||
[TrainerType.TABITHA]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("magma_admin", "magma", [ Species.CAMERUPT ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_aqua_magma_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)),
|
||||
@ -1540,9 +1540,9 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
[TrainerType.FLARE_GRUNT]: new TrainerConfig(++t).setHasGenders("Flare Grunt Female").setHasDouble("Flare Grunts").setMoneyMultiplier(1.0).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene))
|
||||
.setSpeciesPools({
|
||||
[TrainerPoolTier.COMMON]: [ Species.FLETCHLING, Species.LITLEO, Species.PONYTA, Species.INKAY, Species.HOUNDOUR, Species.SKORUPI, Species.SCRAFTY, Species.CROAGUNK, Species.SCATTERBUG, Species.ESPURR ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP ],
|
||||
[TrainerPoolTier.UNCOMMON]: [ Species.HELIOPTILE, Species.ELECTRIKE, Species.SKRELP, Species.PANCHAM, Species.PURRLOIN, Species.POOCHYENA, Species.BINACLE, Species.CLAUNCHER, Species.PUMPKABOO, Species.PHANTUMP, Species.FOONGUS ],
|
||||
[TrainerPoolTier.RARE]: [ Species.LITWICK, Species.SNEASEL, Species.PAWNIARD, Species.SLIGGOO ],
|
||||
[TrainerPoolTier.SUPER_RARE]: [ Species.NOIVERN, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG ]
|
||||
[TrainerPoolTier.SUPER_RARE]: [ Species.NOIBAT, Species.HISUI_SLIGGOO, Species.HISUI_AVALUGG ]
|
||||
}),
|
||||
[TrainerType.BRYONY]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin_female", "flare", [ Species.LIEPARD ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)),
|
||||
[TrainerType.XEROSIC]: new TrainerConfig(++t).setMoneyMultiplier(1.5).initForEvilTeamAdmin("flare_admin", "flare", [ Species.MALAMAR ]).setEncounterBgm(TrainerType.PLASMA_GRUNT).setBattleBgm("battle_plasma_grunt").setMixedBattleBgm("battle_flare_grunt").setVictoryBgm("victory_team_plasma").setPartyTemplateFunc(scene => getEvilGruntPartyTemplate(scene)),
|
||||
@ -1893,7 +1893,10 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
}),
|
||||
|
||||
[TrainerType.ROCKET_BOSS_GIOVANNI_1]: new TrainerConfig(t = TrainerType.ROCKET_BOSS_GIOVANNI_1).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", []).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN, Species.ALOLA_PERSIAN ]))
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.PERSIAN, Species.ALOLA_PERSIAN ], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.gender = Gender.MALE;
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.DUGTRIO, Species.ALOLA_DUGTRIO ]))
|
||||
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.HONCHKROW ]))
|
||||
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.NIDOKING, Species.NIDOQUEEN ]))
|
||||
@ -1945,6 +1948,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.formIndex = 1; // Mega Camerupt
|
||||
p.generateName();
|
||||
p.gender = Gender.MALE;
|
||||
})),
|
||||
[TrainerType.MAXIE_2]: new TrainerConfig(++t).setName("Maxie").initForEvilTeamLeader("Magma Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SOLROCK, Species.TYPHLOSION ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -1967,6 +1971,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.formIndex = 1; // Mega Camerupt
|
||||
p.generateName();
|
||||
p.gender = Gender.MALE;
|
||||
}))
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GROUDON ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -1985,6 +1990,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.formIndex = 1; // Mega Sharpedo
|
||||
p.generateName();
|
||||
p.gender = Gender.MALE;
|
||||
})),
|
||||
[TrainerType.ARCHIE_2]: new TrainerConfig(++t).setName("Archie").initForEvilTeamLeader("Aqua Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.EMPOLEON, Species.LUDICOLO ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -2010,6 +2016,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.formIndex = 1; // Mega Sharpedo
|
||||
p.generateName();
|
||||
p.gender = Gender.MALE;
|
||||
}))
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYOGRE ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -2031,6 +2038,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.gender = Gender.MALE;
|
||||
})),
|
||||
[TrainerType.CYRUS_2]: new TrainerConfig(++t).setName("Cyrus").initForEvilTeamLeader("Galactic Boss", [], true).setMixedBattleBgm("battle_galactic_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.AZELF, Species.UXIE, Species.MESPRIT ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -2049,6 +2057,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.gender = Gender.MALE;
|
||||
}))
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.DARKRAI ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -2065,6 +2074,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.gender = Gender.MALE;
|
||||
})),
|
||||
[TrainerType.GHETSIS_2]: new TrainerConfig(++t).setName("Ghetsis").initForEvilTeamLeader("Plasma Boss", [], true).setMixedBattleBgm("battle_plasma_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GENESECT ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -2084,6 +2094,11 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
if (p.species.speciesId === Species.HYDREIGON) {
|
||||
p.gender = Gender.MALE;
|
||||
} else if (p.species.speciesId === Species.IRON_JUGULIS) {
|
||||
p.gender = Gender.GENDERLESS;
|
||||
}
|
||||
}))
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.KYUREM ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -2105,6 +2120,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.formIndex = 1; // Mega Gyarados
|
||||
p.generateName();
|
||||
p.gender = Gender.MALE;
|
||||
})),
|
||||
[TrainerType.LYSANDRE_2]: new TrainerConfig(++t).setName("Lysandre").initForEvilTeamLeader("Flare Boss", [], true).setMixedBattleBgm("battle_flare_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SCREAM_TAIL, Species.FLUTTER_MANE ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -2124,6 +2140,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.formIndex = 1; // Mega Gyardos
|
||||
p.generateName();
|
||||
p.gender = Gender.MALE;
|
||||
}))
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.YVELTAL ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -2131,7 +2148,10 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.MASTER_BALL;
|
||||
})),
|
||||
[TrainerType.LUSAMINE]: new TrainerConfig(++t).setName("Lusamine").initForEvilTeamLeader("Aether Boss", []).setMixedBattleBgm("battle_aether_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ]))
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.gender = Gender.FEMALE;
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.LILLIGANT, Species.HISUI_LILLIGANT ]))
|
||||
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ]))
|
||||
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALAR_SLOWBRO, Species.GALAR_SLOWKING ]))
|
||||
@ -2148,7 +2168,10 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.ROGUE_BALL;
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ]))
|
||||
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ]))
|
||||
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.gender = Gender.FEMALE;
|
||||
}))
|
||||
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.STAKATAKA, Species.CELESTEELA, Species.GUZZLORD ], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ROGUE_BALL;
|
||||
@ -2191,6 +2214,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
p.generateAndPopulateMoveset();
|
||||
p.gender = Gender.MALE;
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
})),
|
||||
[TrainerType.GUZMA_2]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", [], true).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma")
|
||||
@ -2198,6 +2222,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.setBoss(true, 2);
|
||||
p.generateAndPopulateMoveset();
|
||||
p.abilityIndex = 2; //Anticipation
|
||||
p.gender = Gender.MALE;
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -2239,6 +2264,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.formIndex = 1; // G-Max Copperajah
|
||||
p.generateName();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.gender = Gender.FEMALE;
|
||||
})),
|
||||
[TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => {
|
||||
@ -2262,6 +2288,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.formIndex = 1; // G-Max Copperajah
|
||||
p.generateName();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.gender = Gender.FEMALE;
|
||||
})),
|
||||
[TrainerType.PENNY]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", []).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VAPOREON, Species.JOLTEON, Species.FLAREON ]))
|
||||
@ -2275,8 +2302,9 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.formIndex = Utils.randSeedInt(5, 1); // Heat, Wash, Frost, Fan, or Mow
|
||||
}))
|
||||
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.abilityIndex = 2; // Pixilate
|
||||
p.generateAndPopulateMoveset();
|
||||
p.gender = Gender.FEMALE;
|
||||
}))
|
||||
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -2290,20 +2318,21 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct?
|
||||
}),
|
||||
[TrainerType.PENNY_2]: new TrainerConfig(++t).setName("Cassiopeia").initForEvilTeamLeader("Star Boss", [], true).setMixedBattleBgm("battle_star_boss").setVictoryBgm("victory_team_plasma")
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => {
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form
|
||||
p.abilityIndex = 2; // Pixilate
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
p.gender = Gender.FEMALE;
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ], TrainerSlot.TRAINER, true, p => {
|
||||
p.generateAndPopulateMoveset();
|
||||
p.pokeball = PokeballType.ULTRA_BALL;
|
||||
}))
|
||||
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.WALKING_WAKE, Species.GOUGING_FIRE, Species.RAGING_BOLT ]))
|
||||
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.SYLVEON ], TrainerSlot.TRAINER, true, p => {
|
||||
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.REVAVROOM ], TrainerSlot.TRAINER, true, p => {
|
||||
p.formIndex = Utils.randSeedInt(5, 1); //Random Starmobile form
|
||||
p.generateAndPopulateMoveset();
|
||||
p.abilityIndex = 2; // Pixilate
|
||||
p.pokeball = PokeballType.ROGUE_BALL;
|
||||
}))
|
||||
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.EEVEE ], TrainerSlot.TRAINER, true, p => {
|
||||
p.setBoss(true, 2);
|
||||
@ -2318,7 +2347,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
p.pokeball = PokeballType.MASTER_BALL;
|
||||
}))
|
||||
.setGenModifiersFunc(party => {
|
||||
const teraPokemon = party[3];
|
||||
const teraPokemon = party[0];
|
||||
return [ modifierTypes.TERA_SHARD().generateType([], [ teraPokemon.species.type1 ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(teraPokemon) as PersistentModifier ]; //TODO: is the bang correct?
|
||||
}),
|
||||
[TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true)
|
||||
|
@ -80,6 +80,7 @@ export enum BattlerTagType {
|
||||
DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
|
||||
AUTOTOMIZED = "AUTOTOMIZED",
|
||||
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
|
||||
POWER_TRICK = "POWER_TRICK",
|
||||
HEAL_BLOCK = "HEAL_BLOCK",
|
||||
TORMENT = "TORMENT",
|
||||
TAUNT = "TAUNT",
|
||||
|
@ -579,26 +579,28 @@ export class Arena {
|
||||
* Applies each `ArenaTag` in this Arena, based on which side (self, enemy, or both) is passed in as a parameter
|
||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
||||
* @param side {@linkcode ArenaTagSide} which side's arena tags to apply
|
||||
* @param simulated if `true`, this applies arena tags without changing game state
|
||||
* @param args array of parameters that the called upon tags may need
|
||||
*/
|
||||
applyTagsForSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide, ...args: unknown[]): void {
|
||||
applyTagsForSide(tagType: ArenaTagType | Constructor<ArenaTag>, side: ArenaTagSide, simulated: boolean, ...args: unknown[]): void {
|
||||
let tags = typeof tagType === "string"
|
||||
? this.tags.filter(t => t.tagType === tagType)
|
||||
: this.tags.filter(t => t instanceof tagType);
|
||||
if (side !== ArenaTagSide.BOTH) {
|
||||
tags = tags.filter(t => t.side === side);
|
||||
}
|
||||
tags.forEach(t => t.apply(this, args));
|
||||
tags.forEach(t => t.apply(this, simulated, ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the specified tag to both sides (ie: both user and trainer's tag that match the Tag specified)
|
||||
* by calling {@linkcode applyTagsForSide()}
|
||||
* @param tagType Either an {@linkcode ArenaTagType} string, or an actual {@linkcode ArenaTag} class to filter which ones to apply
|
||||
* @param simulated if `true`, this applies arena tags without changing game state
|
||||
* @param args array of parameters that the called upon tags may need
|
||||
*/
|
||||
applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, ...args: unknown[]): void {
|
||||
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, ...args);
|
||||
applyTags(tagType: ArenaTagType | Constructor<ArenaTag>, simulated: boolean, ...args: unknown[]): void {
|
||||
this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,7 +19,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
||||
import { Status, StatusEffect, getRandomStatus } from "#app/data/status-effect";
|
||||
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
|
||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag } 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 { 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";
|
||||
@ -1538,7 +1538,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder);
|
||||
applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder);
|
||||
|
||||
this.scene.arena.applyTags(ArenaTagType.ION_DELUGE, moveTypeHolder);
|
||||
this.scene.arena.applyTags(ArenaTagType.ION_DELUGE, simulated, moveTypeHolder);
|
||||
if (this.getTag(BattlerTagType.ELECTRIFIED)) {
|
||||
moveTypeHolder.value = Type.ELECTRIC;
|
||||
}
|
||||
@ -2605,7 +2605,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */
|
||||
const screenMultiplier = new Utils.NumberHolder(1);
|
||||
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
|
||||
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, simulated, moveCategory, screenMultiplier);
|
||||
|
||||
/**
|
||||
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
|
||||
@ -2810,15 +2810,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (this.isFainted()) {
|
||||
// set splice index here, so future scene queues happen before FaintedPhase
|
||||
this.scene.setPhaseQueueSplice();
|
||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
|
||||
if (!isNullOrUndefined(destinyTag) && dmg) {
|
||||
// Destiny Bond will activate during FaintPhase
|
||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, source));
|
||||
} else {
|
||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
|
||||
}
|
||||
this.destroySubstitute();
|
||||
this.resetSummonData();
|
||||
}
|
||||
|
||||
if (dmg) {
|
||||
destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -3048,6 +3049,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag instanceof PowerTrickTag) {
|
||||
tag.swapStat(this);
|
||||
}
|
||||
|
||||
this.summonData.tags.push(tag);
|
||||
}
|
||||
|
||||
|
@ -383,7 +383,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||
const battle = this.scene.currentBattle;
|
||||
const template = this.getPartyTemplate();
|
||||
|
||||
let species: PokemonSpecies;
|
||||
let baseSpecies: PokemonSpecies;
|
||||
if (this.config.speciesPools) {
|
||||
const tierValue = Utils.randSeedInt(512);
|
||||
let tier = tierValue >= 156 ? TrainerPoolTier.COMMON : tierValue >= 32 ? TrainerPoolTier.UNCOMMON : tierValue >= 6 ? TrainerPoolTier.RARE : tierValue >= 1 ? TrainerPoolTier.SUPER_RARE : TrainerPoolTier.ULTRA_RARE;
|
||||
@ -393,17 +393,17 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||
tier--;
|
||||
}
|
||||
const tierPool = this.config.speciesPools[tier];
|
||||
species = getPokemonSpecies(Utils.randSeedItem(tierPool));
|
||||
baseSpecies = getPokemonSpecies(Utils.randSeedItem(tierPool));
|
||||
} else {
|
||||
species = this.scene.randomSpecies(battle.waveIndex, level, false, this.config.speciesFilter);
|
||||
baseSpecies = this.scene.randomSpecies(battle.waveIndex, level, false, this.config.speciesFilter);
|
||||
}
|
||||
|
||||
let ret = getPokemonSpecies(species.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex));
|
||||
let ret = getPokemonSpecies(baseSpecies.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex));
|
||||
let retry = false;
|
||||
|
||||
console.log(ret.getName());
|
||||
|
||||
if (pokemonPrevolutions.hasOwnProperty(species.speciesId) && ret.speciesId !== species.speciesId) {
|
||||
if (pokemonPrevolutions.hasOwnProperty(baseSpecies.speciesId) && ret.speciesId !== baseSpecies.speciesId) {
|
||||
retry = true;
|
||||
} else if (template.isBalanced(battle.enemyParty.length)) {
|
||||
const partyMemberTypes = battle.enemyParty.map(p => p.getTypes(true)).flat();
|
||||
@ -417,7 +417,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||
console.log("Attempting reroll of species evolution to fit specialty type...");
|
||||
let evoAttempt = 0;
|
||||
while (retry && evoAttempt++ < 10) {
|
||||
ret = getPokemonSpecies(species.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex));
|
||||
ret = getPokemonSpecies(baseSpecies.getTrainerSpeciesForLevel(level, true, strength, this.scene.currentBattle.waveIndex));
|
||||
console.log(ret.name);
|
||||
if (this.config.specialtyTypes.find(t => ret.isOfType(t))) {
|
||||
retry = false;
|
||||
@ -426,7 +426,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
// Prompts reroll of party member species if species already present in the enemy party
|
||||
if (this.checkDuplicateSpecies(ret)) {
|
||||
if (this.checkDuplicateSpecies(ret, baseSpecies)) {
|
||||
console.log("Duplicate species detected, prompting reroll...");
|
||||
retry = true;
|
||||
}
|
||||
@ -442,13 +442,16 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* Checks if the enemy trainer already has the Pokemon species in their party
|
||||
* @param {PokemonSpecies} species {@linkcode PokemonSpecies}
|
||||
* @param {PokemonSpecies} baseSpecies {@linkcode PokemonSpecies} - baseSpecies of the Pokemon if species is forced to evolve
|
||||
* @returns `true` if the species is already present in the party
|
||||
*/
|
||||
checkDuplicateSpecies(species: PokemonSpecies): boolean {
|
||||
checkDuplicateSpecies(species: PokemonSpecies, baseSpecies: PokemonSpecies): boolean {
|
||||
const staticPartyPokemon = (signatureSpecies[TrainerType[this.config.trainerType]] ?? []).flat(1);
|
||||
|
||||
const currentPartySpecies = this.scene.getEnemyParty().map(p => {
|
||||
return p.species.speciesId;
|
||||
});
|
||||
return currentPartySpecies.includes(species.speciesId);
|
||||
return currentPartySpecies.includes(species.speciesId) || staticPartyPokemon.includes(baseSpecies.speciesId);
|
||||
}
|
||||
|
||||
getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] {
|
||||
|
@ -2227,7 +2227,8 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, base
|
||||
],
|
||||
[
|
||||
new ModifierTypeOption(modifierTypes.HYPER_POTION(), 0, baseCost * 0.8),
|
||||
new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75)
|
||||
new ModifierTypeOption(modifierTypes.MAX_REVIVE(), 0, baseCost * 2.75),
|
||||
new ModifierTypeOption(modifierTypes.MEMORY_MUSHROOM(), 0, baseCost * 4)
|
||||
],
|
||||
[
|
||||
new ModifierTypeOption(modifierTypes.MAX_POTION(), 0, baseCost * 1.5),
|
||||
|
@ -11,7 +11,7 @@ import Pokemon, { type PlayerPokemon } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Overrides from "#app/overrides";
|
||||
import { EvolutionPhase } from "#app/phases/evolution-phase";
|
||||
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||
import { LearnMovePhase, LearnMoveType } from "#app/phases/learn-move-phase";
|
||||
import { LevelUpPhase } from "#app/phases/level-up-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { achvs } from "#app/system/achv";
|
||||
@ -2236,7 +2236,7 @@ export class TmModifier extends ConsumablePokemonModifier {
|
||||
*/
|
||||
override apply(playerPokemon: PlayerPokemon): boolean {
|
||||
|
||||
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, true));
|
||||
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), this.type.moveId, LearnMoveType.TM));
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -2256,8 +2256,9 @@ export class RememberMoveModifier extends ConsumablePokemonModifier {
|
||||
* @param playerPokemon The {@linkcode PlayerPokemon} that should remember the move
|
||||
* @returns always `true`
|
||||
*/
|
||||
override apply(playerPokemon: PlayerPokemon): boolean {
|
||||
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex]));
|
||||
override apply(playerPokemon: PlayerPokemon, cost?: number): boolean {
|
||||
|
||||
playerPokemon.scene.unshiftPhase(new LearnMovePhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.getLearnableLevelMoves()[this.levelMoveIndex], LearnMoveType.MEMORY, cost));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export class EggLapsePhase extends Phase {
|
||||
if (eggsToHatchCount > 0) {
|
||||
if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 1) {
|
||||
this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => {
|
||||
// show prompt for skip
|
||||
// show prompt for skip, blocking inputs for 1 second
|
||||
this.scene.ui.showText(i18next.t("battle:eggSkipPrompt"), 0);
|
||||
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
|
||||
this.hatchEggsSkipped(eggsToHatch);
|
||||
@ -41,7 +41,8 @@ export class EggLapsePhase extends Phase {
|
||||
}, () => {
|
||||
this.hatchEggsRegular(eggsToHatch);
|
||||
this.end();
|
||||
}
|
||||
},
|
||||
null, null, null, 1000, true
|
||||
);
|
||||
}, 100, true);
|
||||
} else if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 2) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { Phase } from "#app/phase";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import EggHatchSceneHandler from "#app/ui/egg-hatch-scene-handler";
|
||||
import { EggHatchData } from "#app/data/egg-hatch-data";
|
||||
|
||||
/**
|
||||
@ -11,7 +10,6 @@ import { EggHatchData } from "#app/data/egg-hatch-data";
|
||||
*/
|
||||
export class EggSummaryPhase extends Phase {
|
||||
private eggHatchData: EggHatchData[];
|
||||
private eggHatchHandler: EggHatchSceneHandler;
|
||||
|
||||
constructor(scene: BattleScene, eggHatchData: EggHatchData[]) {
|
||||
super(scene);
|
||||
@ -26,7 +24,6 @@ export class EggSummaryPhase extends Phase {
|
||||
if (i >= this.eggHatchData.length) {
|
||||
this.scene.ui.setModeForceTransition(Mode.EGG_HATCH_SUMMARY, this.eggHatchData).then(() => {
|
||||
this.scene.fadeOutBgm(undefined, false);
|
||||
this.eggHatchHandler = this.scene.ui.getHandler() as EggHatchSceneHandler;
|
||||
});
|
||||
|
||||
} else {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { BattlerIndex, BattleType } from "#app/battle";
|
||||
import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability";
|
||||
import { BattlerTagLapseType } from "#app/data/battler-tags";
|
||||
import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags";
|
||||
import { battleSpecDialogue } from "#app/data/dialogue";
|
||||
import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move";
|
||||
import { BattleSpec } from "#app/enums/battle-spec";
|
||||
import { StatusEffect } from "#app/enums/status-effect";
|
||||
import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
|
||||
import Pokemon, { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
|
||||
import i18next from "i18next";
|
||||
@ -19,20 +19,40 @@ import { SwitchPhase } from "./switch-phase";
|
||||
import { VictoryPhase } from "./victory-phase";
|
||||
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { isNullOrUndefined } from "#app/utils";
|
||||
import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters";
|
||||
|
||||
export class FaintPhase extends PokemonPhase {
|
||||
/**
|
||||
* Whether or not enduring (for this phase's purposes, Reviver Seed) should be prevented
|
||||
*/
|
||||
private preventEndure: boolean;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure?: boolean) {
|
||||
/**
|
||||
* Destiny Bond tag belonging to the currently fainting Pokemon, if applicable
|
||||
*/
|
||||
private destinyTag?: DestinyBondTag;
|
||||
|
||||
/**
|
||||
* The source Pokemon that dealt fatal damage and should get KO'd by Destiny Bond, if applicable
|
||||
*/
|
||||
private source?: Pokemon;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag, source?: Pokemon) {
|
||||
super(scene, battlerIndex);
|
||||
|
||||
this.preventEndure = preventEndure!; // TODO: is this bang correct?
|
||||
this.preventEndure = preventEndure;
|
||||
this.destinyTag = destinyTag;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
if (!isNullOrUndefined(this.destinyTag) && !isNullOrUndefined(this.source)) {
|
||||
this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM);
|
||||
}
|
||||
|
||||
if (!this.preventEndure) {
|
||||
const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier;
|
||||
|
||||
|
@ -2,24 +2,37 @@ import BattleScene from "#app/battle-scene";
|
||||
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
||||
import Move, { allMoves } from "#app/data/move";
|
||||
import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Overrides from "#app/overrides";
|
||||
import EvolutionSceneHandler from "#app/ui/evolution-scene-handler";
|
||||
import { SummaryUiMode } from "#app/ui/summary-ui-handler";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import i18next from "i18next";
|
||||
import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase";
|
||||
import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
|
||||
export enum LearnMoveType {
|
||||
/** For learning a move via level-up, evolution, or other non-item-based event */
|
||||
LEARN_MOVE,
|
||||
/** For learning a move via Memory Mushroom */
|
||||
MEMORY,
|
||||
/** For learning a move via TM */
|
||||
TM
|
||||
}
|
||||
|
||||
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
private moveId: Moves;
|
||||
private messageMode: Mode;
|
||||
private fromTM: boolean;
|
||||
private learnMoveType;
|
||||
private cost: number;
|
||||
|
||||
constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, fromTM?: boolean) {
|
||||
constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, learnMoveType: LearnMoveType = LearnMoveType.LEARN_MOVE, cost: number = -1) {
|
||||
super(scene, partyMemberIndex);
|
||||
this.moveId = moveId;
|
||||
this.fromTM = fromTM ?? false;
|
||||
this.learnMoveType = learnMoveType;
|
||||
this.cost = cost;
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -136,11 +149,23 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
* @param Pokemon The Pokemon learning the move
|
||||
*/
|
||||
async learnMove(index: number, move: Move, pokemon: Pokemon, textMessage?: string) {
|
||||
if (this.fromTM) {
|
||||
if (this.learnMoveType === LearnMoveType.TM) {
|
||||
if (!pokemon.usedTMs) {
|
||||
pokemon.usedTMs = [];
|
||||
}
|
||||
pokemon.usedTMs.push(this.moveId);
|
||||
this.scene.tryRemovePhase((phase) => phase instanceof SelectModifierPhase);
|
||||
} else if (this.learnMoveType === LearnMoveType.MEMORY) {
|
||||
if (this.cost !== -1) {
|
||||
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
|
||||
this.scene.money -= this.cost;
|
||||
this.scene.updateMoneyText();
|
||||
this.scene.animateMoneyChanged(false);
|
||||
}
|
||||
this.scene.playSound("se/buy");
|
||||
} else {
|
||||
this.scene.tryRemovePhase((phase) => phase instanceof SelectModifierPhase);
|
||||
}
|
||||
}
|
||||
pokemon.setMove(index, this.moveId);
|
||||
initMoveAnim(this.scene, this.moveId).then(() => {
|
||||
|
@ -99,8 +99,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
|
||||
const hasActiveTargets = targets.some(t => t.isActive(true));
|
||||
|
||||
/** Check if the target is immune via ability to the attacking move */
|
||||
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move));
|
||||
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
|
||||
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !targets[0].getTag(SemiInvulnerableTag);
|
||||
|
||||
/**
|
||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
||||
@ -140,7 +141,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const bypassIgnoreProtect = new Utils.BooleanHolder(false);
|
||||
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
|
||||
if (!this.move.getMove().isAllyTarget()) {
|
||||
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
|
||||
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
|
||||
}
|
||||
|
||||
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
|
||||
@ -148,8 +149,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|
||||
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
|
||||
|
||||
/** Is the pokemon immune due to an ablility? */
|
||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move));
|
||||
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
|
||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !target.getTag(SemiInvulnerableTag);
|
||||
|
||||
/**
|
||||
* If the move missed a target, stop all future hits against that target
|
||||
|
@ -128,7 +128,9 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
this.lapsePreMoveAndMoveTags();
|
||||
|
||||
this.resolveFinalPreMoveCancellationChecks();
|
||||
if (!(this.failed || this.cancelled)) {
|
||||
this.resolveFinalPreMoveCancellationChecks();
|
||||
}
|
||||
|
||||
if (this.cancelled || this.failed) {
|
||||
this.handlePreMoveFailures();
|
||||
@ -145,8 +147,9 @@ export class MovePhase extends BattlePhase {
|
||||
const moveQueue = this.pokemon.getMoveQueue();
|
||||
|
||||
if (targets.length === 0 || (moveQueue.length && moveQueue[0].move === Moves.NONE)) {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
this.cancelled = true;
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ export class PostSummonPhase extends PokemonPhase {
|
||||
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
||||
pokemon.status.turnCount = 0;
|
||||
}
|
||||
this.scene.arena.applyTags(ArenaTrapTag, pokemon);
|
||||
this.scene.arena.applyTags(ArenaTrapTag, false, pokemon);
|
||||
|
||||
// If this is mystery encounter and has post summon phase tag, apply post summon effects
|
||||
if (this.scene.currentBattle.isBattleMysteryEncounter() && pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag).length > 0) {
|
||||
|
@ -16,26 +16,32 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
private rerollCount: integer;
|
||||
private modifierTiers?: ModifierTier[];
|
||||
private customModifierSettings?: CustomModifierSettings;
|
||||
private isCopy: boolean;
|
||||
|
||||
constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) {
|
||||
private typeOptions: ModifierTypeOption[];
|
||||
|
||||
constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings, isCopy: boolean = false) {
|
||||
super(scene);
|
||||
|
||||
this.rerollCount = rerollCount;
|
||||
this.modifierTiers = modifierTiers;
|
||||
this.customModifierSettings = customModifierSettings;
|
||||
this.isCopy = isCopy;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
if (!this.rerollCount) {
|
||||
if (!this.rerollCount && !this.isCopy) {
|
||||
this.updateSeed();
|
||||
} else {
|
||||
} else if (this.rerollCount) {
|
||||
this.scene.reroll = false;
|
||||
}
|
||||
|
||||
const party = this.scene.getParty();
|
||||
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
|
||||
if (!this.isCopy) {
|
||||
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
|
||||
}
|
||||
const modifierCount = new Utils.IntegerHolder(3);
|
||||
if (this.isPlayer()) {
|
||||
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
|
||||
@ -54,7 +60,7 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
|
||||
const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value);
|
||||
this.typeOptions = this.getModifierTypeOptions(modifierCount.value);
|
||||
|
||||
const modifierSelectCallback = (rowCursor: integer, cursor: integer) => {
|
||||
if (rowCursor < 0 || cursor < 0) {
|
||||
@ -63,13 +69,13 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
this.scene.ui.revertMode();
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
super.end();
|
||||
}, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)));
|
||||
}, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers)));
|
||||
});
|
||||
return false;
|
||||
}
|
||||
let modifierType: ModifierType;
|
||||
let cost: integer;
|
||||
const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers);
|
||||
const rerollCost = this.getRerollCost(this.scene.lockModifierTiers);
|
||||
switch (rowCursor) {
|
||||
case 0:
|
||||
switch (cursor) {
|
||||
@ -79,7 +85,7 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
return false;
|
||||
} else {
|
||||
this.scene.reroll = true;
|
||||
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[]));
|
||||
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, this.typeOptions.map(o => o.type?.tier).filter(t => t !== undefined) as ModifierTier[]));
|
||||
this.scene.ui.clearText();
|
||||
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
|
||||
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
|
||||
@ -98,13 +104,13 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
const itemModifier = itemModifiers[itemIndex];
|
||||
this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity);
|
||||
} else {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
|
||||
}
|
||||
}, PartyUiHandler.FilterItemMaxStacks);
|
||||
break;
|
||||
case 2:
|
||||
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
|
||||
});
|
||||
break;
|
||||
case 3:
|
||||
@ -115,21 +121,21 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
}
|
||||
this.scene.lockModifierTiers = !this.scene.lockModifierTiers;
|
||||
const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler;
|
||||
uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
uiHandler.setRerollCost(this.getRerollCost(this.scene.lockModifierTiers));
|
||||
uiHandler.updateLockRaritiesText();
|
||||
uiHandler.updateRerollCostText();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case 1:
|
||||
if (typeOptions.length === 0) {
|
||||
if (this.typeOptions.length === 0) {
|
||||
this.scene.ui.clearText();
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
super.end();
|
||||
return true;
|
||||
}
|
||||
if (typeOptions[cursor].type) {
|
||||
modifierType = typeOptions[cursor].type;
|
||||
if (this.typeOptions[cursor].type) {
|
||||
modifierType = this.typeOptions[cursor].type;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -151,8 +157,16 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
}
|
||||
|
||||
const applyModifier = (modifier: Modifier, playSound: boolean = false) => {
|
||||
const result = this.scene.addModifier(modifier, false, playSound);
|
||||
if (cost) {
|
||||
const result = this.scene.addModifier(modifier, false, playSound, undefined, undefined, cost);
|
||||
// Queue a copy of this phase when applying a TM or Memory Mushroom.
|
||||
// If the player selects either of these, then escapes out of consuming them,
|
||||
// they are returned to a shop in the same state.
|
||||
if (modifier.type instanceof RememberMoveModifierType ||
|
||||
modifier.type instanceof TmModifierType) {
|
||||
this.scene.unshiftPhase(this.copy());
|
||||
}
|
||||
|
||||
if (cost && !(modifier.type instanceof RememberMoveModifierType)) {
|
||||
result.then(success => {
|
||||
if (success) {
|
||||
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
|
||||
@ -189,7 +203,7 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
applyModifier(modifier, true);
|
||||
});
|
||||
} else {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
|
||||
}
|
||||
}, modifierType.selectFilter);
|
||||
} else {
|
||||
@ -216,7 +230,7 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
applyModifier(modifier!, true); // TODO: is the bang correct?
|
||||
});
|
||||
} else {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
|
||||
}
|
||||
}, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier);
|
||||
}
|
||||
@ -226,7 +240,7 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
|
||||
return !cost!;// TODO: is the bang correct?
|
||||
};
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), this.typeOptions, modifierSelectCallback, this.getRerollCost(this.scene.lockModifierTiers));
|
||||
}
|
||||
|
||||
updateSeed(): void {
|
||||
@ -237,13 +251,13 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
return true;
|
||||
}
|
||||
|
||||
getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): number {
|
||||
getRerollCost(lockRarities: boolean): number {
|
||||
let baseValue = 0;
|
||||
if (Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
|
||||
return baseValue;
|
||||
} else if (lockRarities) {
|
||||
const tierValues = [ 50, 125, 300, 750, 2000 ];
|
||||
for (const opt of typeOptions) {
|
||||
for (const opt of this.typeOptions) {
|
||||
baseValue += tierValues[opt.type.tier ?? 0];
|
||||
}
|
||||
} else {
|
||||
@ -271,6 +285,16 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings);
|
||||
}
|
||||
|
||||
copy(): SelectModifierPhase {
|
||||
return new SelectModifierPhase(
|
||||
this.scene,
|
||||
this.rerollCount,
|
||||
this.modifierTiers,
|
||||
{ guaranteedModifierTypeOptions: this.typeOptions, rerollMultiplier: this.customModifierSettings?.rerollMultiplier, allowLuckUpgrades: false },
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
addModifier(modifier: Modifier): Promise<boolean> {
|
||||
return this.scene.addModifier(modifier, false, true);
|
||||
}
|
||||
|
@ -64,8 +64,7 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
|
||||
if (!this.selfTarget && stages.value < 0) {
|
||||
// TODO: Include simulate boolean when tag applications can be simulated
|
||||
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
|
||||
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, cancelled);
|
||||
}
|
||||
|
||||
if (!cancelled.value && !this.selfTarget && stages.value < 0) {
|
||||
|
@ -45,7 +45,7 @@ export class TurnStartPhase extends FieldPhase {
|
||||
|
||||
// Next, a check for Trick Room is applied to determine sort order.
|
||||
const speedReversed = new Utils.BooleanHolder(false);
|
||||
this.scene.arena.applyTags(TrickRoomTag, speedReversed);
|
||||
this.scene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
||||
|
||||
// Adjust the sort function based on whether Trick Room is active.
|
||||
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
|
||||
|
@ -32,7 +32,7 @@ describe("Abilities - POWER CONSTRUCT", () => {
|
||||
});
|
||||
|
||||
test(
|
||||
"check if fainted pokemon switches to base form on arena reset",
|
||||
"check if fainted 50% Power Construct Pokemon switches to base form on arena reset",
|
||||
async () => {
|
||||
const baseForm = 2,
|
||||
completeForm = 4;
|
||||
@ -41,7 +41,37 @@ describe("Abilities - POWER CONSTRUCT", () => {
|
||||
[Species.ZYGARDE]: completeForm,
|
||||
});
|
||||
|
||||
await game.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]);
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]);
|
||||
|
||||
const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE);
|
||||
expect(zygarde).not.toBe(undefined);
|
||||
expect(zygarde!.formIndex).toBe(completeForm);
|
||||
|
||||
zygarde!.hp = 0;
|
||||
zygarde!.status = new Status(StatusEffect.FAINT);
|
||||
expect(zygarde!.isFainted()).toBe(true);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.doKillOpponents();
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.doSelectModifier();
|
||||
await game.phaseInterceptor.to(QuietFormChangePhase);
|
||||
|
||||
expect(zygarde!.formIndex).toBe(baseForm);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"check if fainted 10% Power Construct Pokemon switches to base form on arena reset",
|
||||
async () => {
|
||||
const baseForm = 3,
|
||||
completeForm = 5;
|
||||
game.override.startingWave(4);
|
||||
game.override.starterForms({
|
||||
[Species.ZYGARDE]: completeForm,
|
||||
});
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP, Species.ZYGARDE ]);
|
||||
|
||||
const zygarde = game.scene.getParty().find((p) => p.species.speciesId === Species.ZYGARDE);
|
||||
expect(zygarde).not.toBe(undefined);
|
||||
|
@ -71,4 +71,23 @@ describe("Abilities - Volt Absorb", () => {
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => {
|
||||
game.override.moveset(Moves.THUNDERBOLT);
|
||||
game.override.enemyMoveset(Moves.DIVE);
|
||||
game.override.enemySpecies(Species.MAGIKARP);
|
||||
game.override.enemyAbility(Abilities.VOLT_ABSORB);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.THUNDERBOLT);
|
||||
enemyPokemon.hp = enemyPokemon.hp - 1;
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
await game.move.forceMiss();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phase from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -32,15 +33,16 @@ describe("Items - Lock Capsule", () => {
|
||||
});
|
||||
|
||||
it("doesn't set the cost of common tier items to 0", async () => {
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
game.scene.overridePhase(new SelectModifierPhase(game.scene, 0, undefined, { guaranteedModifierTiers: [ ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON ], fillRemaining: false }));
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
|
||||
const selectModifierPhase = game.scene.getCurrentPhase() as SelectModifierPhase;
|
||||
const rerollCost = selectModifierPhase.getRerollCost(true);
|
||||
expect(rerollCost).toBe(150);
|
||||
});
|
||||
|
||||
const rewards = game.scene.getCurrentPhase() as SelectModifierPhase;
|
||||
const potion = new ModifierTypeOption(modifierTypes.POTION(), 0, 40); // Common tier item
|
||||
const rerollCost = rewards.getRerollCost([ potion, potion, potion ], true);
|
||||
|
||||
expect(rerollCost).toBe(150);
|
||||
game.doSelectModifier();
|
||||
await game.phaseInterceptor.to("SelectModifierPhase");
|
||||
}, 20000);
|
||||
});
|
||||
|
@ -111,7 +111,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
|
||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
if (defender.scene.arena.getTagOnSide(ArenaTagType.AURORA_VEIL, side)) {
|
||||
defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, move.category, defender.scene.currentBattle.double, multiplierHolder);
|
||||
defender.scene.arena.applyTagsForSide(ArenaTagType.AURORA_VEIL, side, false, move.category, multiplierHolder);
|
||||
}
|
||||
|
||||
return move.power * multiplierHolder.value;
|
||||
|
255
src/test/moves/destiny_bond.test.ts
Normal file
255
src/test/moves/destiny_bond.test.ts
Normal file
@ -0,0 +1,255 @@
|
||||
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
|
||||
|
||||
|
||||
describe("Moves - Destiny Bond", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
const defaultParty = [ Species.BULBASAUR, Species.SQUIRTLE ];
|
||||
const enemyFirst = [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ];
|
||||
const playerFirst = [ BattlerIndex.PLAYER, BattlerIndex.ENEMY ];
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleType("single")
|
||||
.ability(Abilities.UNNERVE) // Pre-emptively prevent flakiness from opponent berries
|
||||
.enemySpecies(Species.RATTATA)
|
||||
.enemyAbility(Abilities.RUN_AWAY)
|
||||
.startingLevel(100) // Make sure tested moves KO
|
||||
.enemyLevel(5)
|
||||
.enemyMoveset(Moves.DESTINY_BOND);
|
||||
});
|
||||
|
||||
it("should KO the opponent on the same turn", async () => {
|
||||
const moveToUse = Moves.TACKLE;
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
it("should KO the opponent on the next turn", async () => {
|
||||
const moveToUse = Moves.TACKLE;
|
||||
|
||||
game.override.moveset([ Moves.SPLASH, moveToUse ]);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
// Turn 1: Enemy uses Destiny Bond and doesn't faint
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.setTurnOrder(playerFirst);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(false);
|
||||
expect(playerPokemon?.isFainted()).toBe(false);
|
||||
|
||||
// Turn 2: Player KO's the enemy before the enemy's turn
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(playerFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail if used twice in a row", async () => {
|
||||
const moveToUse = Moves.TACKLE;
|
||||
|
||||
game.override.moveset([ Moves.SPLASH, moveToUse ]);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
// Turn 1: Enemy uses Destiny Bond and doesn't faint
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(false);
|
||||
expect(playerPokemon?.isFainted()).toBe(false);
|
||||
|
||||
// Turn 2: Enemy should fail Destiny Bond then get KO'd
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not KO the opponent if the user dies to weather", async () => {
|
||||
// Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm
|
||||
const moveToUse = Moves.FALSE_SWIPE;
|
||||
|
||||
game.override.moveset(moveToUse)
|
||||
.ability(Abilities.SAND_STREAM);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not KO the opponent if the user had another turn", async () => {
|
||||
const moveToUse = Moves.TACKLE;
|
||||
|
||||
game.override.moveset([ Moves.SPORE, moveToUse ]);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
// Turn 1: Enemy uses Destiny Bond and doesn't faint
|
||||
game.move.select(Moves.SPORE);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(false);
|
||||
expect(playerPokemon?.isFainted()).toBe(false);
|
||||
expect(enemyPokemon?.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
// Turn 2: Enemy should skip a turn due to sleep, then get KO'd
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not KO an ally", async () => {
|
||||
game.override.moveset([ Moves.DESTINY_BOND, Moves.CRUNCH ])
|
||||
.battleType("double");
|
||||
await game.classicMode.startBattle([ Species.SHEDINJA, Species.BULBASAUR, Species.SQUIRTLE ]);
|
||||
|
||||
const enemyPokemon0 = game.scene.getEnemyField()[0];
|
||||
const enemyPokemon1 = game.scene.getEnemyField()[1];
|
||||
const playerPokemon0 = game.scene.getPlayerField()[0];
|
||||
const playerPokemon1 = game.scene.getPlayerField()[1];
|
||||
|
||||
// Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch
|
||||
game.move.select(Moves.DESTINY_BOND, 0);
|
||||
game.move.select(Moves.CRUNCH, 1, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon0?.isFainted()).toBe(false);
|
||||
expect(enemyPokemon1?.isFainted()).toBe(false);
|
||||
expect(playerPokemon0?.isFainted()).toBe(true);
|
||||
expect(playerPokemon1?.isFainted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not cause a crash if the user is KO'd by Ceaseless Edge", async () => {
|
||||
const moveToUse = Moves.CEASELESS_EDGE;
|
||||
vi.spyOn(allMoves[moveToUse], "accuracy", "get").mockReturnValue(100);
|
||||
|
||||
game.override.moveset(moveToUse);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(true);
|
||||
|
||||
// Ceaseless Edge spikes effect should still activate
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
|
||||
expect(tagAfter.layers).toBe(1);
|
||||
});
|
||||
|
||||
it("should not cause a crash if the user is KO'd by Pledge moves", async () => {
|
||||
game.override.moveset([ Moves.GRASS_PLEDGE, Moves.WATER_PLEDGE ])
|
||||
.battleType("double");
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon0 = game.scene.getEnemyField()[0];
|
||||
const enemyPokemon1 = game.scene.getEnemyField()[1];
|
||||
const playerPokemon0 = game.scene.getPlayerField()[0];
|
||||
const playerPokemon1 = game.scene.getPlayerField()[1];
|
||||
|
||||
game.move.select(Moves.GRASS_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.WATER_PLEDGE, 1, BattlerIndex.ENEMY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon0?.isFainted()).toBe(true);
|
||||
expect(enemyPokemon1?.isFainted()).toBe(false);
|
||||
expect(playerPokemon0?.isFainted()).toBe(false);
|
||||
expect(playerPokemon1?.isFainted()).toBe(true);
|
||||
|
||||
// Pledge secondary effect should still activate
|
||||
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, ArenaTagSide.ENEMY) as ArenaTrapTag;
|
||||
expect(tagAfter.tagType).toBe(ArenaTagType.GRASS_WATER_PLEDGE);
|
||||
});
|
||||
|
||||
/**
|
||||
* In particular, this should prevent something like
|
||||
* {@link https://github.com/pagefaultgames/pokerogue/issues/4219}
|
||||
* from occurring with fainting by KO'ing a Destiny Bond user with U-Turn.
|
||||
*/
|
||||
it("should not allow the opponent to revive via Reviver Seed", async () => {
|
||||
const moveToUse = Moves.TACKLE;
|
||||
|
||||
game.override.moveset(moveToUse)
|
||||
.startingHeldItems([{ name: "REVIVER_SEED" }]);
|
||||
await game.classicMode.startBattle(defaultParty);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.move.select(moveToUse);
|
||||
await game.setTurnOrder(enemyFirst);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemyPokemon?.isFainted()).toBe(true);
|
||||
expect(playerPokemon?.isFainted()).toBe(true);
|
||||
|
||||
// Check that the Tackle user's Reviver Seed did not activate
|
||||
const revSeeds = game.scene.getModifiers(PokemonInstantReviveModifier).filter(m => m.pokemonId === playerPokemon?.id);
|
||||
expect(revSeeds.length).toBe(1);
|
||||
});
|
||||
});
|
@ -139,4 +139,58 @@ describe("Moves - Dragon Tail", () => {
|
||||
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should force a switch upon fainting an opponent normally", async () => {
|
||||
game.override.startingWave(5)
|
||||
.startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([ Species.DRATINI ]);
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Make sure the enemy switched to a healthy Pokemon
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy).toBeDefined();
|
||||
expect(enemy.isFullHp()).toBe(true);
|
||||
|
||||
// Make sure the enemy has a fainted Pokemon in their party and not on the field
|
||||
const faintedEnemy = game.scene.getEnemyParty().find(p => !p.isAllowedInBattle());
|
||||
expect(faintedEnemy).toBeDefined();
|
||||
expect(game.scene.getEnemyField().length).toBe(1);
|
||||
});
|
||||
|
||||
it("should not cause a softlock when activating an opponent trainer's reviver seed", async () => {
|
||||
game.override.startingWave(5)
|
||||
.enemyHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([ Species.DRATINI ]);
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Make sure the enemy field is not empty and has a revived Pokemon
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy).toBeDefined();
|
||||
expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2));
|
||||
expect(game.scene.getEnemyField().length).toBe(1);
|
||||
});
|
||||
|
||||
it("should not cause a softlock when activating a player's reviver seed", async () => {
|
||||
game.override.startingHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.enemyMoveset(Moves.DRAGON_TAIL)
|
||||
.enemyLevel(1000); // To make sure Dragon Tail KO's the player
|
||||
await game.classicMode.startBattle([ Species.DRATINI, Species.BULBASAUR ]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Make sure the player's field is not empty and has a revived Pokemon
|
||||
const dratini = game.scene.getPlayerPokemon()!;
|
||||
expect(dratini).toBeDefined();
|
||||
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
|
||||
expect(game.scene.getPlayerField().length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
|
||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
if (defender.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, side)) {
|
||||
defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, move.category, defender.scene.currentBattle.double, multiplierHolder);
|
||||
defender.scene.arena.applyTagsForSide(ArenaTagType.LIGHT_SCREEN, side, false, move.category, multiplierHolder);
|
||||
}
|
||||
|
||||
return move.power * multiplierHolder.value;
|
||||
|
113
src/test/moves/power_trick.test.ts
Normal file
113
src/test/moves/power_trick.test.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Species } from "#enums/species";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
|
||||
describe("Moves - Power Trick", () => {
|
||||
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")
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemySpecies(Species.MEW)
|
||||
.enemyLevel(200)
|
||||
.moveset([ Moves.POWER_TRICK ])
|
||||
.ability(Abilities.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("swaps the user's ATK and DEF stats", async () => {
|
||||
await game.classicMode.startBattle([ Species.SHUCKLE ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const baseATK = player.getStat(Stat.ATK, false);
|
||||
const baseDEF = player.getStat(Stat.DEF, false);
|
||||
|
||||
game.move.select(Moves.POWER_TRICK);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStat(Stat.ATK, false)).toBe(baseDEF);
|
||||
expect(player.getStat(Stat.DEF, false)).toBe(baseATK);
|
||||
expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeDefined();
|
||||
});
|
||||
|
||||
it("resets initial ATK and DEF stat swap when used consecutively", async () => {
|
||||
await game.classicMode.startBattle([ Species.SHUCKLE ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const baseATK = player.getStat(Stat.ATK, false);
|
||||
const baseDEF = player.getStat(Stat.DEF, false);
|
||||
|
||||
game.move.select(Moves.POWER_TRICK);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
game.move.select(Moves.POWER_TRICK);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(player.getStat(Stat.ATK, false)).toBe(baseATK);
|
||||
expect(player.getStat(Stat.DEF, false)).toBe(baseDEF);
|
||||
expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should pass effect when using BATON_PASS", async () => {
|
||||
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
|
||||
await game.override.moveset([ Moves.POWER_TRICK, Moves.BATON_PASS ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
player.addTag(BattlerTagType.POWER_TRICK);
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
const switchedPlayer = game.scene.getPlayerPokemon()!;
|
||||
const baseATK = switchedPlayer.getStat(Stat.ATK);
|
||||
const baseDEF = switchedPlayer.getStat(Stat.DEF);
|
||||
|
||||
expect(switchedPlayer.getStat(Stat.ATK, false)).toBe(baseDEF);
|
||||
expect(switchedPlayer.getStat(Stat.DEF, false)).toBe(baseATK);
|
||||
expect(switchedPlayer.getTag(BattlerTagType.POWER_TRICK)).toBeDefined();
|
||||
});
|
||||
|
||||
it("should remove effect after using Transform", async () => {
|
||||
await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]);
|
||||
await game.override.moveset([ Moves.POWER_TRICK, Moves.TRANSFORM ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
player.addTag(BattlerTagType.POWER_TRICK);
|
||||
|
||||
game.move.select(Moves.TRANSFORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
const baseATK = enemy.getStat(Stat.ATK);
|
||||
const baseDEF = enemy.getStat(Stat.DEF);
|
||||
|
||||
expect(player.getStat(Stat.ATK, false)).toBe(baseATK);
|
||||
expect(player.getStat(Stat.DEF, false)).toBe(baseDEF);
|
||||
expect(player.getTag(BattlerTagType.POWER_TRICK)).toBeUndefined();
|
||||
});
|
||||
});
|
@ -94,7 +94,7 @@ const getMockedMoveDamage = (defender: Pokemon, attacker: Pokemon, move: Move) =
|
||||
const side = defender.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
|
||||
if (defender.scene.arena.getTagOnSide(ArenaTagType.REFLECT, side)) {
|
||||
defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, move.category, defender.scene.currentBattle.double, multiplierHolder);
|
||||
defender.scene.arena.applyTagsForSide(ArenaTagType.REFLECT, side, false, move.category, multiplierHolder);
|
||||
}
|
||||
|
||||
return move.power * multiplierHolder.value;
|
||||
|
89
src/test/moves/secret_power.test.ts
Normal file
89
src/test/moves/secret_power.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { allMoves, SecretPowerAttr } from "#app/data/move";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||
|
||||
describe("Moves - Secret Power", () => {
|
||||
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.SECRET_POWER ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyLevel(60)
|
||||
.enemyAbility(Abilities.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("Secret Power checks for an active terrain first then looks at the biome for its secondary effect", async () => {
|
||||
game.override
|
||||
.startingBiome(Biome.VOLCANO)
|
||||
.enemyMoveset([ Moves.SPLASH, Moves.MISTY_TERRAIN ]);
|
||||
vi.spyOn(allMoves[Moves.SECRET_POWER], "chance", "get").mockReturnValue(100);
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// No Terrain + Biome.VOLCANO --> Burn
|
||||
game.move.select(Moves.SECRET_POWER);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
// Misty Terrain --> SpAtk -1
|
||||
game.move.select(Moves.SECRET_POWER);
|
||||
await game.forceEnemyMove(Moves.MISTY_TERRAIN);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
|
||||
});
|
||||
|
||||
it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect",
|
||||
async () => {
|
||||
game.override
|
||||
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ])
|
||||
.enemyMoveset([ Moves.SPLASH ])
|
||||
.battleType("double");
|
||||
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||
|
||||
const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0];
|
||||
vi.spyOn(secretPowerAttr, "getMoveChance");
|
||||
|
||||
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30);
|
||||
}
|
||||
);
|
||||
});
|
@ -96,4 +96,23 @@ describe("Moves - U-turn", () => {
|
||||
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
}, 20000);
|
||||
|
||||
it("still forces a switch if u-turn KO's the opponent", async () => {
|
||||
game.override.startingLevel(1000); // Ensure that U-Turn KO's the opponent
|
||||
await game.classicMode.startBattle([
|
||||
Species.RAICHU,
|
||||
Species.SHUCKLE
|
||||
]);
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// KO the opponent with U-Turn
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
|
||||
// Check that U-Turn forced a switch
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
||||
});
|
||||
});
|
||||
|
@ -124,10 +124,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should start battle against the trainer", async () => {
|
||||
it("should start battle against the trainer with correctly loaded assets", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
|
||||
|
||||
let successfullyLoaded = false;
|
||||
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
|
||||
const ace = scene.currentBattle?.enemyParty[0];
|
||||
if (ace) {
|
||||
// Pretend that loading assets takes an extra 500ms
|
||||
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
successfullyLoaded = true;
|
||||
resolve();
|
||||
}, 500);
|
||||
}));
|
||||
}
|
||||
|
||||
return scene.currentBattle?.enemyParty ?? [];
|
||||
});
|
||||
|
||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||
|
||||
// Check that assets are successfully loaded
|
||||
expect(successfullyLoaded).toBe(true);
|
||||
|
||||
// Check usual battle stuff
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||
expect(scene.currentBattle.trainer).toBeDefined();
|
||||
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
|
||||
@ -182,10 +203,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should start battle against the trainer", async () => {
|
||||
it("should start battle against the trainer with correctly loaded assets", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
|
||||
|
||||
let successfullyLoaded = false;
|
||||
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
|
||||
const ace = scene.currentBattle?.enemyParty[0];
|
||||
if (ace) {
|
||||
// Pretend that loading assets takes an extra 500ms
|
||||
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
successfullyLoaded = true;
|
||||
resolve();
|
||||
}, 500);
|
||||
}));
|
||||
}
|
||||
|
||||
return scene.currentBattle?.enemyParty ?? [];
|
||||
});
|
||||
|
||||
await runMysteryEncounterToEnd(game, 2, undefined, true);
|
||||
|
||||
// Check that assets are successfully loaded
|
||||
expect(successfullyLoaded).toBe(true);
|
||||
|
||||
// Check usual battle stuff
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||
expect(scene.currentBattle.trainer).toBeDefined();
|
||||
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
|
||||
@ -240,10 +282,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should start battle against the trainer", async () => {
|
||||
it("should start battle against the trainer with correctly loaded assets", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
|
||||
|
||||
let successfullyLoaded = false;
|
||||
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
|
||||
const ace = scene.currentBattle?.enemyParty[0];
|
||||
if (ace) {
|
||||
// Pretend that loading assets takes an extra 500ms
|
||||
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
successfullyLoaded = true;
|
||||
resolve();
|
||||
}, 500);
|
||||
}));
|
||||
}
|
||||
|
||||
return scene.currentBattle?.enemyParty ?? [];
|
||||
});
|
||||
|
||||
await runMysteryEncounterToEnd(game, 3, undefined, true);
|
||||
|
||||
// Check that assets are successfully loaded
|
||||
expect(successfullyLoaded).toBe(true);
|
||||
|
||||
// Check usual battle stuff
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||
expect(scene.currentBattle.trainer).toBeDefined();
|
||||
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
|
||||
|
@ -63,11 +63,11 @@ describe("SelectModifierPhase", () => {
|
||||
new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000)
|
||||
];
|
||||
|
||||
const selectModifierPhase1 = new SelectModifierPhase(scene);
|
||||
const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { rerollMultiplier: 2 });
|
||||
const selectModifierPhase1 = new SelectModifierPhase(scene, 0, undefined, { guaranteedModifierTypeOptions: options });
|
||||
const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { guaranteedModifierTypeOptions: options, rerollMultiplier: 2 });
|
||||
|
||||
const cost1 = selectModifierPhase1.getRerollCost(options, false);
|
||||
const cost2 = selectModifierPhase2.getRerollCost(options, false);
|
||||
const cost1 = selectModifierPhase1.getRerollCost(false);
|
||||
const cost2 = selectModifierPhase2.getRerollCost(false);
|
||||
expect(cost2).toEqual(cost1 * 2);
|
||||
});
|
||||
|
||||
|
@ -165,6 +165,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
|
||||
if (this.config.delay) {
|
||||
this.blockInput = true;
|
||||
this.optionSelectText.setAlpha(0.5);
|
||||
this.cursorObj?.setAlpha(0.8);
|
||||
this.scene.time.delayedCall(Utils.fixedInt(this.config.delay), () => this.unblockInput());
|
||||
}
|
||||
|
||||
@ -256,6 +257,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
|
||||
|
||||
this.blockInput = false;
|
||||
this.optionSelectText.setAlpha(1);
|
||||
this.cursorObj?.setAlpha(1);
|
||||
}
|
||||
|
||||
getOptionsWithScroll(): OptionSelectItem[] {
|
||||
|
@ -76,7 +76,8 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
|
||||
}
|
||||
}
|
||||
],
|
||||
delay: args.length >= 6 && args[5] !== null ? args[5] as integer : 0
|
||||
delay: args.length >= 6 && args[5] !== null ? args[5] as number : 0,
|
||||
noCancel: args.length >= 7 && args[6] !== null ? args[6] as boolean : false,
|
||||
};
|
||||
|
||||
super.show([ config ]);
|
||||
@ -96,7 +97,7 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
|
||||
}
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
if (button === Button.CANCEL && this.blockInput) {
|
||||
if (button === Button.CANCEL && this.blockInput && !this.config?.noCancel) {
|
||||
this.unblockInput();
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,9 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
|
||||
private scrollGridHandler : ScrollableGridUiHandler;
|
||||
private cursorObj: Phaser.GameObjects.Image;
|
||||
|
||||
/** used to add a delay before which it is not possible to exit the summary */
|
||||
private blockExit: boolean;
|
||||
|
||||
/**
|
||||
* Allows subscribers to listen for events
|
||||
*
|
||||
@ -168,6 +171,13 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
|
||||
this.setCursor(0);
|
||||
|
||||
this.scene.playSoundWithoutBgm("evolution_fanfare");
|
||||
|
||||
// Prevent exiting the egg summary for 2 seconds if the egg hatching
|
||||
// was skipped automatically and for 1 second otherwise
|
||||
const exitBlockingDuration = (this.scene.eggSkipPreference === 2) ? 2000 : 1000;
|
||||
this.blockExit = true;
|
||||
this.scene.time.delayedCall(exitBlockingDuration, () => this.blockExit = false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -203,13 +213,17 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
|
||||
const ui = this.getUi();
|
||||
|
||||
let success = false;
|
||||
const error = false;
|
||||
let error = false;
|
||||
if (button === Button.CANCEL) {
|
||||
const phase = this.scene.getCurrentPhase();
|
||||
if (phase instanceof EggSummaryPhase) {
|
||||
phase.end();
|
||||
if (!this.blockExit) {
|
||||
const phase = this.scene.getCurrentPhase();
|
||||
if (phase instanceof EggSummaryPhase) {
|
||||
phase.end();
|
||||
}
|
||||
success = true;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
success = true;
|
||||
} else {
|
||||
this.scrollGridHandler.processInput(button);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
|
||||
import { IntegerHolder } from "./../utils";
|
||||
import Phaser from "phaser";
|
||||
|
||||
export const SHOP_OPTIONS_ROW_LIMIT = 6;
|
||||
export const SHOP_OPTIONS_ROW_LIMIT = 7;
|
||||
|
||||
export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
||||
private modifierContainer: Phaser.GameObjects.Container;
|
||||
@ -211,7 +211,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
||||
const row = m < SHOP_OPTIONS_ROW_LIMIT ? 0 : 1;
|
||||
const col = m < SHOP_OPTIONS_ROW_LIMIT ? m : m - SHOP_OPTIONS_ROW_LIMIT;
|
||||
const rowOptions = shopTypeOptions.slice(row ? SHOP_OPTIONS_ROW_LIMIT : 0, row ? undefined : SHOP_OPTIONS_ROW_LIMIT);
|
||||
const sliceWidth = (this.scene.game.canvas.width / SHOP_OPTIONS_ROW_LIMIT) / (rowOptions.length + 2);
|
||||
const sliceWidth = (this.scene.game.canvas.width / 6) / (rowOptions.length + 2);
|
||||
const option = new ModifierOption(this.scene, sliceWidth * (col + 1) + (sliceWidth * 0.5), ((-this.scene.game.canvas.height / 12) - (this.scene.game.canvas.height / 32) - (40 - (28 * row - 1))), shopTypeOptions[m]);
|
||||
option.setScale(0.375);
|
||||
this.scene.add.existing(option);
|
||||
|
@ -671,6 +671,9 @@ export default class PartyUiHandler extends MessageUiHandler {
|
||||
} else if (this.cursor === 6) {
|
||||
this.partyCancelButton.select();
|
||||
}
|
||||
if (this.lastCursor < 6 && this.lastCursor >= party.length) {
|
||||
this.lastCursor = party.length - 1;
|
||||
}
|
||||
|
||||
for (const p in party) {
|
||||
const slotIndex = parseInt(p);
|
||||
|
@ -587,13 +587,13 @@ export default class RunInfoUiHandler extends UiHandler {
|
||||
const typeText = typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]";
|
||||
rules.push(typeText);
|
||||
break;
|
||||
case Challenges.FRESH_START:
|
||||
rules.push(i18next.t("challenges:freshStart.name"));
|
||||
break;
|
||||
case Challenges.INVERSE_BATTLE:
|
||||
//
|
||||
rules.push(i18next.t("challenges:inverseBattle.shortName"));
|
||||
break;
|
||||
default:
|
||||
const localisationKey = Challenges[this.runInfo.challenges[i].id].split("_").map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("");
|
||||
rules.push(i18next.t(`challenges:${localisationKey}.name`));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user