Merge branch 'pagefaultgames:main' into flingImplementation
13336
public/images/items.json
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 132 KiB |
BIN
public/images/items/light_ball.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
public/images/items/metal_powder.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
public/images/items/quick_powder.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
public/images/items/thick_club.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
@ -219,48 +219,6 @@
|
||||
"h": 23
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "354-mega_2",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 26,
|
||||
"h": 30
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 176,
|
||||
"w": 26,
|
||||
"h": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "354-mega_3",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 26,
|
||||
"h": 30
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 206,
|
||||
"w": 26,
|
||||
"h": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "666-archipelago_2",
|
||||
"rotated": false,
|
||||
@ -1479,27 +1437,6 @@
|
||||
"h": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "779_2",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 10,
|
||||
"y": 7,
|
||||
"w": 21,
|
||||
"h": 20
|
||||
},
|
||||
"frame": {
|
||||
"x": 107,
|
||||
"y": 155,
|
||||
"w": 21,
|
||||
"h": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "673_2",
|
||||
"rotated": false,
|
||||
@ -1521,27 +1458,6 @@
|
||||
"h": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "779_3",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 10,
|
||||
"y": 7,
|
||||
"w": 21,
|
||||
"h": 20
|
||||
},
|
||||
"frame": {
|
||||
"x": 107,
|
||||
"y": 175,
|
||||
"w": 21,
|
||||
"h": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "666-tundra_3",
|
||||
"rotated": false,
|
||||
@ -1794,48 +1710,6 @@
|
||||
"h": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "743_2",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 12,
|
||||
"y": 5,
|
||||
"w": 20,
|
||||
"h": 22
|
||||
},
|
||||
"frame": {
|
||||
"x": 78,
|
||||
"y": 358,
|
||||
"w": 20,
|
||||
"h": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "743_3",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 12,
|
||||
"y": 5,
|
||||
"w": 20,
|
||||
"h": 22
|
||||
},
|
||||
"frame": {
|
||||
"x": 78,
|
||||
"y": 380,
|
||||
"w": 20,
|
||||
"h": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "690_3",
|
||||
"rotated": false,
|
||||
@ -1857,48 +1731,6 @@
|
||||
"h": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "742_2",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 12,
|
||||
"y": 5,
|
||||
"w": 19,
|
||||
"h": 20
|
||||
},
|
||||
"frame": {
|
||||
"x": 78,
|
||||
"y": 423,
|
||||
"w": 19,
|
||||
"h": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "742_3",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 12,
|
||||
"y": 5,
|
||||
"w": 19,
|
||||
"h": 20
|
||||
},
|
||||
"frame": {
|
||||
"x": 78,
|
||||
"y": 443,
|
||||
"w": 19,
|
||||
"h": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "720_1",
|
||||
"rotated": false,
|
||||
@ -2214,48 +2046,6 @@
|
||||
"h": 17
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "777_2",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 13,
|
||||
"y": 8,
|
||||
"w": 17,
|
||||
"h": 19
|
||||
},
|
||||
"frame": {
|
||||
"x": 111,
|
||||
"y": 396,
|
||||
"w": 17,
|
||||
"h": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "777_3",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 13,
|
||||
"y": 8,
|
||||
"w": 17,
|
||||
"h": 19
|
||||
},
|
||||
"frame": {
|
||||
"x": 111,
|
||||
"y": 415,
|
||||
"w": 17,
|
||||
"h": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "712_2",
|
||||
"rotated": false,
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.3 KiB |
@ -9,7 +9,7 @@ import { Weather, WeatherType } from "./weather";
|
||||
import { BattlerTag, GroundedTag } from "./battler-tags";
|
||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||
import { Gender } from "./gender";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr } from "./move";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit } from "./move";
|
||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||
import { Stat, getStatName } from "./pokemon-stat";
|
||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||
@ -1231,6 +1231,84 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for abilities that convert single-strike moves to two-strike moves (i.e. Parental Bond).
|
||||
* @param damageMultiplier the damage multiplier for the second strike, relative to the first.
|
||||
*/
|
||||
export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
|
||||
private damageMultiplier: number;
|
||||
|
||||
constructor(damageMultiplier: number) {
|
||||
super(false);
|
||||
|
||||
this.damageMultiplier = damageMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this attribute can apply to a given move.
|
||||
* @param {Move} move the move to which this attribute may apply
|
||||
* @param numTargets the number of {@linkcode Pokemon} targeted by this move
|
||||
* @returns true if the attribute can apply to the move, false otherwise
|
||||
*/
|
||||
canApplyPreAttack(move: Move, numTargets: integer): boolean {
|
||||
/**
|
||||
* Parental Bond cannot apply to multi-hit moves, charging moves, or
|
||||
* moves that cause the user to faint.
|
||||
*/
|
||||
const exceptAttrs: Constructor<MoveAttr>[] = [
|
||||
MultiHitAttr,
|
||||
ChargeAttr,
|
||||
SacrificialAttr,
|
||||
SacrificialAttrOnHit
|
||||
];
|
||||
|
||||
/** Parental Bond cannot apply to these specific moves */
|
||||
const exceptMoves: Moves[] = [
|
||||
Moves.FLING,
|
||||
Moves.UPROAR,
|
||||
Moves.ROLLOUT,
|
||||
Moves.ICE_BALL,
|
||||
Moves.ENDEAVOR
|
||||
];
|
||||
|
||||
/** Also check if this move is an Attack move and if it's only targeting one Pokemon */
|
||||
return numTargets === 1
|
||||
&& !exceptAttrs.some(attr => move.hasAttr(attr))
|
||||
&& !exceptMoves.some(id => move.id === id)
|
||||
&& move.category !== MoveCategory.STATUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* If conditions are met, this doubles the move's hit count (via args[1])
|
||||
* or multiplies the damage of secondary strikes (via args[2])
|
||||
* @param {Pokemon} pokemon the Pokemon using the move
|
||||
* @param passive n/a
|
||||
* @param defender n/a
|
||||
* @param {Move} move the move used by the ability source
|
||||
* @param args\[0\] the number of Pokemon this move is targeting
|
||||
* @param {Utils.IntegerHolder} args\[1\] the number of strikes with this move
|
||||
* @param {Utils.NumberHolder} args\[2\] the damage multiplier for the current strike
|
||||
* @returns
|
||||
*/
|
||||
applyPreAttack(pokemon: Pokemon, passive: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
|
||||
const numTargets = args[0] as integer;
|
||||
const hitCount = args[1] as Utils.IntegerHolder;
|
||||
const multiplier = args[2] as Utils.NumberHolder;
|
||||
|
||||
if (this.canApplyPreAttack(move, numTargets)) {
|
||||
if (!!hitCount?.value) {
|
||||
hitCount.value *= 2;
|
||||
}
|
||||
|
||||
if (!!multiplier?.value && pokemon.turnData.hitsLeft % 2 === 1 && pokemon.turnData.hitsLeft !== pokemon.turnData.hitCount) {
|
||||
multiplier.value *= this.damageMultiplier;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for abilities that boost the damage of moves
|
||||
* For abilities that boost the base power of moves, see VariableMovePowerAbAttr
|
||||
@ -4632,7 +4710,7 @@ export function initAbilities() {
|
||||
new Ability(Abilities.AERILATE, 6)
|
||||
.attr(MoveTypeChangeAttr, Type.FLYING, 1.2, (user, target, move) => move.type === Type.NORMAL),
|
||||
new Ability(Abilities.PARENTAL_BOND, 6)
|
||||
.unimplemented(),
|
||||
.attr(AddSecondStrikeAbAttr, 0.25),
|
||||
new Ability(Abilities.DARK_AURA, 6)
|
||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => getPokemonMessage(pokemon, " is radiating a Dark Aura!"))
|
||||
.attr(FieldMoveTypePowerBoostAbAttr, Type.DARK, 4 / 3),
|
||||
|
@ -896,6 +896,12 @@ export class ProtectedTag extends BattlerTag {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
new CommonBattleAnim(CommonAnim.PROTECT, pokemon).play(pokemon.scene);
|
||||
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsProtectedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
|
||||
// Stop multi-hit moves early
|
||||
const effectPhase = pokemon.scene.getCurrentPhase();
|
||||
if (effectPhase instanceof MoveEffectPhase) {
|
||||
effectPhase.stopMultiHit(pokemon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -812,11 +812,14 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
public trigger: MoveEffectTrigger;
|
||||
/** Should this effect only apply on the first hit? */
|
||||
public firstHitOnly: boolean;
|
||||
/** Should this effect only apply on the last hit? */
|
||||
public lastHitOnly: boolean;
|
||||
|
||||
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false) {
|
||||
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false) {
|
||||
super(selfTarget);
|
||||
this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY;
|
||||
this.firstHitOnly = firstHitOnly;
|
||||
this.lastHitOnly = lastHitOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1065,7 +1068,7 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
private unblockable: boolean;
|
||||
|
||||
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
|
||||
super(true);
|
||||
super(true, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
|
||||
this.useHp = useHp;
|
||||
this.damageRatio = damageRatio;
|
||||
@ -1086,8 +1089,8 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.currDamageDealt : user.getMaxHp()) * this.damageRatio),
|
||||
user.turnData.currDamageDealt ? 1 : 0);
|
||||
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio),
|
||||
user.turnData.damageDealt ? 1 : 0);
|
||||
if (!recoilDamage) {
|
||||
return false;
|
||||
}
|
||||
@ -2014,7 +2017,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
* @param ...effects - List of status effects to cure
|
||||
*/
|
||||
constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
|
||||
super(selfTarget);
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
|
||||
this.effects = effects;
|
||||
}
|
||||
@ -2919,7 +2922,7 @@ export class DoublePowerChanceAttr extends VariablePowerAttr {
|
||||
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
|
||||
constructor(limit: integer, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
|
||||
super((user: Pokemon, target: Pokemon, move: Move): number => {
|
||||
const moveHistory = user.getMoveHistory().reverse().slice(1);
|
||||
const moveHistory = user.getLastXMoves(limit + 1).slice(1);
|
||||
|
||||
let count = 0;
|
||||
let turnMove: TurnMove;
|
||||
@ -3288,8 +3291,13 @@ export class PunishmentPowerAttr extends VariablePowerAttr {
|
||||
|
||||
export class PresentPowerAttr extends VariablePowerAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
/**
|
||||
* If this move is multi-hit, and this attribute is applied to any hit
|
||||
* other than the first, this move cannot result in a heal.
|
||||
*/
|
||||
const firstHit = (user.turnData.hitCount === user.turnData.hitsLeft);
|
||||
|
||||
const powerSeed = Utils.randSeedInt(100);
|
||||
const powerSeed = Utils.randSeedInt(firstHit ? 100 : 80);
|
||||
if (powerSeed <= 40) {
|
||||
(args[0] as Utils.NumberHolder).value = 40;
|
||||
} else if (40 < powerSeed && powerSeed <= 70) {
|
||||
@ -3297,6 +3305,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
|
||||
} else if (70 < powerSeed && powerSeed <= 80) {
|
||||
(args[0] as Utils.NumberHolder).value = 120;
|
||||
} else if (80 < powerSeed && powerSeed <= 100) {
|
||||
// If this move is multi-hit, disable all other hits
|
||||
user.stopMultiHit();
|
||||
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
|
||||
Math.max(Math.floor(target.getMaxHp() / 4), 1), getPokemonMessage(target, " regained\nhealth!"), true));
|
||||
}
|
||||
@ -4054,8 +4064,8 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
public turnCountMax: integer;
|
||||
private failOnOverlap: boolean;
|
||||
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer) {
|
||||
super(selfTarget);
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false) {
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly);
|
||||
|
||||
this.tagType = tagType;
|
||||
this.turnCountMin = turnCountMin;
|
||||
@ -4220,7 +4230,7 @@ export class ConfuseAttr extends AddBattlerTagAttr {
|
||||
|
||||
export class RechargeAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
super(BattlerTagType.RECHARGING, true);
|
||||
super(BattlerTagType.RECHARGING, true, false, 1, 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4617,7 +4627,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
private batonPass: boolean;
|
||||
|
||||
constructor(user?: boolean, batonPass?: boolean) {
|
||||
super(false, MoveEffectTrigger.POST_APPLY, true);
|
||||
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
this.user = !!user;
|
||||
this.batonPass = !!batonPass;
|
||||
}
|
||||
@ -4732,7 +4742,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
|
||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||
|
||||
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY);
|
||||
super(true, MoveEffectTrigger.POST_TARGET);
|
||||
this.removedType = removedType;
|
||||
this.messageCallback = messageCallback;
|
||||
|
||||
@ -5540,7 +5550,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
|
||||
|
||||
export class MoneyAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
super(true, MoveEffectTrigger.HIT, true);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
@ -7096,7 +7106,7 @@ export function initMoves() {
|
||||
.target(MoveTarget.BOTH_SIDES)
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.SMACK_DOWN, Type.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, 100, 0, 5)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
||||
@ -7480,14 +7490,14 @@ export function initMoves() {
|
||||
.triageMove(),
|
||||
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.THOUSAND_WAVES, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.LANDS_WRATH, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
@ -7629,7 +7639,7 @@ export function initMoves() {
|
||||
new SelfStatusMove(Moves.BANEFUL_BUNKER, Type.POISON, -1, 10, -1, 4, 7)
|
||||
.attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER),
|
||||
new AttackMove(Moves.SPIRIT_SHACKLE, Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.DARKEST_LARIAT, Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
|
||||
.attr(IgnoreOpponentStatChangesAttr),
|
||||
@ -7673,7 +7683,7 @@ export function initMoves() {
|
||||
.attr(HealOnAllyAttr, 0.5, true, false)
|
||||
.ballBombMove(),
|
||||
new AttackMove(Moves.ANCHOR_SHOT, Type.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, 100, 0, 7)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true),
|
||||
new StatusMove(Moves.PSYCHIC_TERRAIN, Type.PSYCHIC, -1, 10, -1, 0, 7)
|
||||
.attr(TerrainChangeAttr, TerrainType.PSYCHIC)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
@ -8396,7 +8406,7 @@ export function initMoves() {
|
||||
.attr(RemoveScreensAttr),
|
||||
new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
|
||||
.attr(MoneyAttr)
|
||||
.attr(StatChangeAttr, BattleStat.SPATK, -1, true, null, true, true)
|
||||
.attr(StatChangeAttr, BattleStat.SPATK, -1, true, null, true, false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1)
|
||||
|
@ -3327,7 +3327,7 @@ export const starterPassiveAbilities = {
|
||||
[Species.KRABBY]: Abilities.UNBURDEN,
|
||||
[Species.VOLTORB]: Abilities.ELECTRIC_SURGE,
|
||||
[Species.EXEGGCUTE]: Abilities.RIPEN,
|
||||
[Species.CUBONE]: Abilities.HUGE_POWER,
|
||||
[Species.CUBONE]: Abilities.PARENTAL_BOND,
|
||||
[Species.LICKITUNG]: Abilities.THICK_FAT,
|
||||
[Species.KOFFING]: Abilities.PARENTAL_BOND,
|
||||
[Species.RHYHORN]: Abilities.FILTER,
|
||||
|
@ -10,20 +10,20 @@ import * as Utils from "../utils";
|
||||
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
|
||||
import { getLevelTotalExp } from "../data/exp";
|
||||
import { Stat } from "../data/pokemon-stat";
|
||||
import { AttackTypeBoosterModifier, DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonMultiHitModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, EvolutionStatBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
|
||||
import { AttackTypeBoosterModifier, DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonMultiHitModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
|
||||
import { PokeballType } from "../data/pokeball";
|
||||
import { Gender } from "../data/gender";
|
||||
import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
|
||||
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
||||
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||
import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases";
|
||||
import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases";
|
||||
import { BattleStat } from "../data/battle-stat";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, TypeImmuneTag, getBattlerTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr } from "../data/ability";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr } from "../data/ability";
|
||||
import PokemonData from "../system/pokemon-data";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
@ -657,7 +657,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
|
||||
}
|
||||
const statValue = new Utils.NumberHolder(this.getStat(stat));
|
||||
this.scene.applyModifiers(EvolutionStatBoosterModifier, this.isPlayer(), this, stat, statValue);
|
||||
this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
|
||||
|
||||
const fieldApplied = new Utils.BooleanHolder(false);
|
||||
for (const pokemon of this.scene.getField(true)) {
|
||||
@ -1800,6 +1800,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
source.stopMultiHit(this);
|
||||
result = HitResult.NO_EFFECT;
|
||||
} else {
|
||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
|
||||
@ -1885,8 +1886,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
|
||||
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
|
||||
|
||||
const effectPhase = this.scene.getCurrentPhase();
|
||||
let numTargets = 1;
|
||||
if (effectPhase instanceof MoveEffectPhase) {
|
||||
numTargets = effectPhase.getTargets().length;
|
||||
}
|
||||
const twoStrikeMultiplier = new Utils.NumberHolder(1);
|
||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier);
|
||||
|
||||
if (!isTypeImmune) {
|
||||
damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier.value);
|
||||
damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier.value);
|
||||
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
||||
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
||||
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
||||
@ -2251,6 +2260,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.summonData.moveQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this Pokemon is using a multi-hit move, cancels all subsequent strikes
|
||||
* @param {Pokemon} target If specified, this only cancels subsequent strikes against this Pokemon
|
||||
*/
|
||||
stopMultiHit(target?: Pokemon): void {
|
||||
const effectPhase = this.scene.getCurrentPhase();
|
||||
if (effectPhase instanceof MoveEffectPhase) {
|
||||
effectPhase.stopMultiHit(target);
|
||||
}
|
||||
}
|
||||
|
||||
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
||||
@ -3385,7 +3405,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
|
||||
this.trainerSlot = trainerSlot;
|
||||
if (boss) {
|
||||
this.setBoss();
|
||||
this.setBoss(boss, dataSource?.bossSegments);
|
||||
}
|
||||
|
||||
if (Overrides.OPP_STATUS_OVERRIDE) {
|
||||
|
@ -35,6 +35,7 @@ export interface ModifierTypeTranslationEntry {
|
||||
|
||||
export interface ModifierTypeTranslationEntries {
|
||||
ModifierType: { [key: string]: ModifierTypeTranslationEntry },
|
||||
SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry },
|
||||
AttackTypeBoosterItem: SimpleTranslationEntries,
|
||||
TempBattleStatBoosterItem: SimpleTranslationEntries,
|
||||
TempBattleStatBoosterStatName: SimpleTranslationEntries,
|
||||
|
@ -242,6 +242,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_FUSED_CHANCE": { "name": "Fusionsmarke", "description": "Fügt eine 1%ige Chance hinzu, dass ein wildes Pokémon eine Fusion ist." },
|
||||
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "Kugelblitz", description: "Ein Item, das von Pikachu getragen werden kann. Es erhöht den Angriff und den Spezial-Angriff." },
|
||||
"THICK_CLUB": { name: "Kampfknochen", description: "Ein Item, das von Tragosso oder Knogga getragen werden kann. Dieser harte Knochen erhöht den Angriff." },
|
||||
"METAL_POWDER": { name: "Metallstaub", description: "Ein Item, das von Ditto getragen werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Verteidigung." },
|
||||
"QUICK_POWDER": { name: "Flottstaub", description: "Ein Item, das Ditto zum Tragen gegeben werden kann. Fein und doch hart, erhöht dieses sonderbare Pulver die Initiative." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "X-Angriff",
|
||||
"x_defense": "X-Verteidigung",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "Endure Token" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "Fusion Token", description: "Adds a 1% chance that a wild Pokémon will be a fusion." },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "Light Ball", description: "It's a mysterious orb that boosts Pikachu's Attack and Sp. Atk stats." },
|
||||
"THICK_CLUB": { name: "Thick Club", description: "This hard bone of unknown origin boosts Cubone or Marowak's Attack stat." },
|
||||
"METAL_POWDER": { name: "Metal Powder", description: "Extremely fine yet hard, this odd powder boosts Ditto's Defense stat." },
|
||||
"QUICK_POWDER": { name: "Quick Powder", description: "Extremely fine yet hard, this odd powder boosts Ditto's Speed stat." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "X Attack",
|
||||
"x_defense": "X Defense",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "Ficha Aguante" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "Ficha Fusión", description: "Agrega un 1% de probabilidad de que un Pokémon salvaje sea una fusión." },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "Bola Luminosa", description: "Asombrosa esfera que aumenta el Ataque y el Ataque Especial. Debe llevarla Pikachu." },
|
||||
"THICK_CLUB": { name: "Hueso Grueso", description: "Extraño tipo de hueso que potencia los ataques físicos. Debe llevarlo Cubone o Marowak." },
|
||||
"METAL_POWDER": { name: "Polvo Metálico", description: "Polvo muy fino, pero a la vez poderoso, que aumenta la Defensa. Debe llevarlo Ditto." },
|
||||
"QUICK_POWDER": { name: "Polvo Veloz", description: "Polvo muy fino, pero a la vez poderoso, que aumenta la Velocidad. Debe llevarlo Ditto." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "Ataque X",
|
||||
"x_defense": "Defensa X",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "Jeton Ténacité" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "Jeton Fusion", description: "Ajoute 1% de chances qu’un Pokémon sauvage soit une fusion." },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "Balle Lumière", description: "Objet à faire tenir à Pikachu. Un orbe énigmatique qui augmente son Attaque et son Attaque Spéciale." },
|
||||
"THICK_CLUB": { name: "Masse Os", description: "Objet à faire tenir à Osselait ou Ossatueur. Un os dur qui augmente leur Attaque." },
|
||||
"METAL_POWDER": { name: "Poudre Métal", description: "Objet à faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, augmente sa Défense." },
|
||||
"QUICK_POWDER": { name: "Poudre Vite", description: "Objet à faire tenir à Métamorph. Cette poudre étrange, très fine mais résistante, augmente sa Vitesse." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "Attaque +",
|
||||
"x_defense": "Défense +",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "Gettone di Resistenza" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "Gettone della fusione", description: "Aggiunge l'1% di possibilità che un Pokémon selvatico sia una fusione." },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "Elettropalla", description: "Strumento da dare a Pikachu. Sfera insolita che aumenta l’Attacco e l’Attacco Speciale." },
|
||||
"THICK_CLUB": { name: "Osso spesso", description: "Strumento da dare a Cubone o Marowak. Osso duro che aumenta l’Attacco." },
|
||||
"METAL_POWDER": { name: "Metalpolvere", description: "Strumento da dare a Ditto. Strana polvere finissima e al tempo stesso dura che migliora la Difesa." },
|
||||
"QUICK_POWDER": { name: "Velopolvere", description: "Strumento da dare a Ditto. Questa strana polvere, fine e al contempo dura, aumenta la Velocità." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "Attacco X",
|
||||
"x_defense": "Difesa X",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "버티기 토큰" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "합체 토큰", description: "야생 포켓몬이 합체되어 등장할 확률이 1% 추가된다." },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "전기구슬", description: "피카츄에게 지니게 하면 공격과 특수공격이 올라가는 이상한 구슬." },
|
||||
"THICK_CLUB": { name: "굵은뼈", description: "무언가의 단단한 뼈. 탕구리 혹은 텅구리에게 지니게 하면 공격이 올라간다." },
|
||||
"METAL_POWDER": { name: "금속파우더", description: "메타몽에게 지니게 하면 방어가 올라가는 이상한 가루. 매우 잘고 단단하다." },
|
||||
"QUICK_POWDER": { name: "스피드파우더", description: "메타몽에게 지니게 하면 스피드가 올라가는 이상한 가루. 매우 잘고 단단하다." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "플러스파워",
|
||||
"x_defense": "디펜드업",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "Token de Persistência" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "Token de Fusão", description: "Adiciona uma chance de 1% de que um Pokémon selvagem seja uma fusão." },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "Bola de Luz", description: "Orbe intrigante que aumenta os atributos de Ataque e Ataque Esp. de Pikachu." },
|
||||
"THICK_CLUB": { name: "Osso Grosso", description: "Este duro osso de origem desconhecida aumenta o atributo de Ataque de Cubone ou Marowak." },
|
||||
"METAL_POWDER": { name: "Pó Metálico", description: "Extremamente fino, porém duro, este pó estranho aumenta o atributo de Defesa de Ditto." },
|
||||
"QUICK_POWDER": { name: "Pó Veloz", description: "Extremamente fino, porém duro, este pó estranho aumenta o atributo de Velocidade de Ditto." }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "Ataque X",
|
||||
"x_defense": "Defesa X",
|
||||
|
@ -241,6 +241,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
"ENEMY_ENDURE_CHANCE": { name: "忍受硬币" },
|
||||
"ENEMY_FUSED_CHANCE": { name: "融合硬币", description: "增加1%野生融合宝可梦出现概率。" },
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "电气球", description: "让皮卡丘携带后,攻击和特攻就会 提高的神奇之球。" },
|
||||
"THICK_CLUB": { name: "粗骨头", description: "某种坚硬的骨头。让卡拉卡拉或嘎啦嘎啦携带后,攻击就会提高。" },
|
||||
"METAL_POWDER": { name: "金属粉", description: "让百变怪携带后,防御就会提高的神奇粉末。非常细腻坚硬。" },
|
||||
"QUICK_POWDER": { name: "速度粉", description: "让百变怪携带后,速度就会提高的神奇粉末。非常细腻坚硬。" }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
"x_attack": "力量强化",
|
||||
"x_defense": "防御强化",
|
||||
|
@ -301,6 +301,12 @@ export const modifierType: ModifierTypeTranslationEntries = {
|
||||
description: "增加1%野生融合寶可夢出現概率。",
|
||||
},
|
||||
},
|
||||
SpeciesBoosterItem: {
|
||||
"LIGHT_BALL": { name: "電氣球", description: "讓皮卡丘攜帶後,攻擊和特攻就會 提高的神奇之球。" },
|
||||
"THICK_CLUB": { name: "粗骨頭", description: "某種堅硬的骨頭。讓卡拉卡拉或嘎啦嘎啦攜帶後,攻擊就會提高。" },
|
||||
"METAL_POWDER": { name: "金屬粉", description: "讓百變怪攜帶後,防禦就會提高的神奇粉末。非常細緻堅硬。" },
|
||||
"QUICK_POWDER": { name: "速度粉", description: "讓百變怪攜帶後,速度就會提高的神奇粉末。非常細緻堅硬。" }
|
||||
},
|
||||
TempBattleStatBoosterItem: {
|
||||
x_attack: "力量強化",
|
||||
x_defense: "防禦強化",
|
||||
|
@ -26,6 +26,7 @@ import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
|
||||
const outputModifierData = false;
|
||||
const useMaxWeightForOutput = false;
|
||||
@ -539,6 +540,28 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i
|
||||
}
|
||||
}
|
||||
|
||||
export type SpeciesStatBoosterItem = keyof typeof SpeciesStatBoosterModifierTypeGenerator.items;
|
||||
|
||||
/**
|
||||
* Modifier type for {@linkcode Modifiers.SpeciesStatBoosterModifier}
|
||||
* @extends PokemonHeldItemModifierType
|
||||
* @implements GeneratedPersistentModifierType
|
||||
*/
|
||||
export class SpeciesStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
|
||||
private key: SpeciesStatBoosterItem;
|
||||
|
||||
constructor(key: SpeciesStatBoosterItem) {
|
||||
const item = SpeciesStatBoosterModifierTypeGenerator.items[key];
|
||||
super(`modifierType:SpeciesBoosterItem.${key}`, key.toLowerCase(), (type, args) => new Modifiers.SpeciesStatBoosterModifier(type, (args[0] as Pokemon).id, item.stats, item.multiplier, item.species));
|
||||
|
||||
this.id = this.key = key;
|
||||
}
|
||||
|
||||
getPregenArgs(): any[] {
|
||||
return [ this.key ];
|
||||
}
|
||||
}
|
||||
|
||||
export class PokemonLevelIncrementModifierType extends PokemonModifierType {
|
||||
constructor(localeKey: string, iconImage: string) {
|
||||
super(localeKey, iconImage, (_type, args) => new Modifiers.PokemonLevelIncrementModifier(this, (args[0] as PlayerPokemon).id), (_pokemon: PlayerPokemon) => null);
|
||||
@ -870,6 +893,81 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which
|
||||
* encapsulates the logic for weighting the most useful held item from
|
||||
* the current list of {@linkcode items}.
|
||||
* @extends ModifierTypeGenerator
|
||||
*/
|
||||
class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator {
|
||||
/** Object comprised of the currently available species-based stat boosting held items */
|
||||
public static items = {
|
||||
LIGHT_BALL: { stats: [Stat.ATK, Stat.SPATK], multiplier: 2, species: [Species.PIKACHU] },
|
||||
THICK_CLUB: { stats: [Stat.ATK], multiplier: 2, species: [Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK] },
|
||||
METAL_POWDER: { stats: [Stat.DEF], multiplier: 2, species: [Species.DITTO] },
|
||||
QUICK_POWDER: { stats: [Stat.SPD], multiplier: 2, species: [Species.DITTO] },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super((party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs) {
|
||||
return new SpeciesStatBoosterModifierType(pregenArgs[0] as SpeciesStatBoosterItem);
|
||||
}
|
||||
|
||||
const values = Object.values(SpeciesStatBoosterModifierTypeGenerator.items);
|
||||
const keys = Object.keys(SpeciesStatBoosterModifierTypeGenerator.items);
|
||||
const weights = keys.map(() => 0);
|
||||
|
||||
for (const p of party) {
|
||||
const speciesId = p.getSpeciesForm(true).speciesId;
|
||||
const fusionSpeciesId = p.isFusion() ? p.getFusionSpeciesForm(true).speciesId : null;
|
||||
const hasFling = p.getMoveset(true).some(m => m.moveId === Moves.FLING);
|
||||
|
||||
for (const i in values) {
|
||||
const checkedSpecies = values[i].species;
|
||||
const checkedStats = values[i].stats;
|
||||
|
||||
// If party member already has the item being weighted currently, skip to the next item
|
||||
const hasItem = p.getHeldItems().some(m => m instanceof Modifiers.SpeciesStatBoosterModifier
|
||||
&& (m as Modifiers.SpeciesStatBoosterModifier).contains(checkedSpecies[0], checkedStats[0]));
|
||||
|
||||
if (!hasItem) {
|
||||
if (checkedSpecies.includes(speciesId) || (!!fusionSpeciesId && checkedSpecies.includes(fusionSpeciesId))) {
|
||||
// Add weight if party member has a matching species or, if applicable, a matching fusion species
|
||||
weights[i]++;
|
||||
} else if (checkedSpecies.includes(Species.PIKACHU) && hasFling) {
|
||||
// Add weight to Light Ball if party member has Fling
|
||||
weights[i]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let totalWeight = 0;
|
||||
for (const weight of weights) {
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (totalWeight !== 0) {
|
||||
const randInt = Utils.randSeedInt(totalWeight, 1);
|
||||
let weight = 0;
|
||||
|
||||
for (const i in weights) {
|
||||
if (weights[i] !== 0) {
|
||||
const curWeight = weight + weights[i];
|
||||
if (randInt <= weight + weights[i]) {
|
||||
return new SpeciesStatBoosterModifierType(keys[i] as SpeciesStatBoosterItem);
|
||||
}
|
||||
weight = curWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TmModifierTypeGenerator extends ModifierTypeGenerator {
|
||||
constructor(tier: ModifierTier) {
|
||||
super((party: Pokemon[]) => {
|
||||
@ -1110,6 +1208,8 @@ export const modifierTypes = {
|
||||
SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 10),
|
||||
MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 25),
|
||||
|
||||
SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(),
|
||||
|
||||
TEMP_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs) {
|
||||
return new TempBattleStatBoosterModifierType(pregenArgs[0] as TempBattleStat);
|
||||
@ -1285,12 +1385,13 @@ const modifierPool: ModifierPool = {
|
||||
new WeightedModifierType(modifierTypes.LURE, 2),
|
||||
new WeightedModifierType(modifierTypes.TEMP_STAT_BOOSTER, 4),
|
||||
new WeightedModifierType(modifierTypes.BERRY, 2),
|
||||
new WeightedModifierType(modifierTypes.TM_COMMON, 1),
|
||||
new WeightedModifierType(modifierTypes.TM_COMMON, 2),
|
||||
].map(m => {
|
||||
m.setTier(ModifierTier.COMMON); return m;
|
||||
}),
|
||||
[ModifierTier.GREAT]: [
|
||||
new WeightedModifierType(modifierTypes.GREAT_BALL, 6),
|
||||
new WeightedModifierType(modifierTypes.PP_UP, 2),
|
||||
new WeightedModifierType(modifierTypes.FULL_HEAL, (party: Pokemon[]) => {
|
||||
const statusEffectPartyMemberCount = Math.min(party.filter(p => p.hp && !!p.status && !p.getHeldItems().some(i => {
|
||||
if (i instanceof Modifiers.TurnStatusEffectModifier) {
|
||||
@ -1344,7 +1445,7 @@ const modifierPool: ModifierPool = {
|
||||
return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8);
|
||||
}, 8),
|
||||
new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1),
|
||||
new WeightedModifierType(modifierTypes.TM_GREAT, 2),
|
||||
new WeightedModifierType(modifierTypes.TM_GREAT, 3),
|
||||
new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => {
|
||||
if (!party.find(p => p.getLearnableLevelMoves().length)) {
|
||||
return 0;
|
||||
@ -1360,15 +1461,15 @@ const modifierPool: ModifierPool = {
|
||||
m.setTier(ModifierTier.GREAT); return m;
|
||||
}),
|
||||
[ModifierTier.ULTRA]: [
|
||||
new WeightedModifierType(modifierTypes.ULTRA_BALL, 24),
|
||||
new WeightedModifierType(modifierTypes.ULTRA_BALL, 15),
|
||||
new WeightedModifierType(modifierTypes.MAX_LURE, 4),
|
||||
new WeightedModifierType(modifierTypes.BIG_NUGGET, skipInLastClassicWaveOrDefault(12)),
|
||||
new WeightedModifierType(modifierTypes.PP_UP, 9),
|
||||
new WeightedModifierType(modifierTypes.PP_MAX, 3),
|
||||
new WeightedModifierType(modifierTypes.MINT, 4),
|
||||
new WeightedModifierType(modifierTypes.RARE_EVOLUTION_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15) * 4, 32), 32),
|
||||
new WeightedModifierType(modifierTypes.AMULET_COIN, 3),
|
||||
//new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => party.some(p => ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions))) && !p.getHeldItems().some(i => i instanceof Modifiers.EvolutionStatBoosterModifier)) ? 10 : 0),
|
||||
new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 12),
|
||||
new WeightedModifierType(modifierTypes.TOXIC_ORB, (party: Pokemon[]) => {
|
||||
const checkedAbilities = [Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.TOXIC_BOOST, Abilities.POISON_HEAL, Abilities.MAGIC_GUARD];
|
||||
const checkedMoves = [Moves.FACADE, Moves.TRICK, Moves.FLING, Moves.SWITCHEROO, Moves.PSYCHO_SHIFT];
|
||||
@ -1383,40 +1484,40 @@ const modifierPool: ModifierPool = {
|
||||
}, 10),
|
||||
new WeightedModifierType(modifierTypes.REVIVER_SEED, 4),
|
||||
new WeightedModifierType(modifierTypes.CANDY_JAR, 5),
|
||||
new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 10),
|
||||
new WeightedModifierType(modifierTypes.TM_ULTRA, 8),
|
||||
new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9),
|
||||
new WeightedModifierType(modifierTypes.TM_ULTRA, 11),
|
||||
new WeightedModifierType(modifierTypes.RARER_CANDY, 4),
|
||||
new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, 2),
|
||||
new WeightedModifierType(modifierTypes.IV_SCANNER, 4),
|
||||
new WeightedModifierType(modifierTypes.EXP_CHARM, 8),
|
||||
new WeightedModifierType(modifierTypes.EXP_SHARE, 12),
|
||||
new WeightedModifierType(modifierTypes.EXP_BALANCE, 4),
|
||||
new WeightedModifierType(modifierTypes.EXP_SHARE, 10),
|
||||
new WeightedModifierType(modifierTypes.EXP_BALANCE, 3),
|
||||
new WeightedModifierType(modifierTypes.TERA_ORB, (party: Pokemon[]) => Math.min(Math.max(Math.floor(party[0].scene.currentBattle.waveIndex / 50) * 2, 1), 4), 4),
|
||||
new WeightedModifierType(modifierTypes.QUICK_CLAW, 3),
|
||||
new WeightedModifierType(modifierTypes.WIDE_LENS, 4),
|
||||
].map(m => {
|
||||
m.setTier(ModifierTier.ULTRA); return m;
|
||||
}),
|
||||
[ModifierTier.ROGUE]: [
|
||||
new WeightedModifierType(modifierTypes.ROGUE_BALL, 24),
|
||||
new WeightedModifierType(modifierTypes.ROGUE_BALL, 16),
|
||||
new WeightedModifierType(modifierTypes.RELIC_GOLD, skipInLastClassicWaveOrDefault(2)),
|
||||
new WeightedModifierType(modifierTypes.LEFTOVERS, 3),
|
||||
new WeightedModifierType(modifierTypes.SHELL_BELL, 3),
|
||||
new WeightedModifierType(modifierTypes.BERRY_POUCH, 4),
|
||||
new WeightedModifierType(modifierTypes.GRIP_CLAW, 5),
|
||||
new WeightedModifierType(modifierTypes.BATON, 2),
|
||||
new WeightedModifierType(modifierTypes.SOUL_DEW, 8),
|
||||
new WeightedModifierType(modifierTypes.SOUL_DEW, 7),
|
||||
//new WeightedModifierType(modifierTypes.OVAL_CHARM, 6),
|
||||
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4),
|
||||
new WeightedModifierType(modifierTypes.ABILITY_CHARM, 6),
|
||||
new WeightedModifierType(modifierTypes.FOCUS_BAND, 5),
|
||||
new WeightedModifierType(modifierTypes.QUICK_CLAW, 3),
|
||||
new WeightedModifierType(modifierTypes.KINGS_ROCK, 3),
|
||||
new WeightedModifierType(modifierTypes.LOCK_CAPSULE, 3),
|
||||
new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, 10),
|
||||
new WeightedModifierType(modifierTypes.FORM_CHANGE_ITEM, 18),
|
||||
new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 8, 32),
|
||||
new WeightedModifierType(modifierTypes.DYNAMAX_BAND, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 8, 32),
|
||||
new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(5 - rerollCount * 2, 0) : 0, 5),
|
||||
new WeightedModifierType(modifierTypes.SUPER_EXP_CHARM, 8),
|
||||
new WeightedModifierType(modifierTypes.FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24),
|
||||
new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36),
|
||||
new WeightedModifierType(modifierTypes.DYNAMAX_BAND, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36),
|
||||
new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, 3),
|
||||
].map(m => {
|
||||
m.setTier(ModifierTier.ROGUE); return m;
|
||||
}),
|
||||
@ -1425,7 +1526,7 @@ const modifierPool: ModifierPool = {
|
||||
new WeightedModifierType(modifierTypes.SHINY_CHARM, 14),
|
||||
new WeightedModifierType(modifierTypes.HEALING_CHARM, 18),
|
||||
new WeightedModifierType(modifierTypes.MULTI_LENS, 18),
|
||||
new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(6 - rerollCount * 2, 0) : 0, 6),
|
||||
new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5),
|
||||
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24),
|
||||
new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE] ? 1 : 0, 1),
|
||||
].map(m => {
|
||||
|
@ -23,6 +23,7 @@ import { Nature } from "#app/data/nature";
|
||||
import * as Overrides from "../overrides";
|
||||
import { ModifierType, modifierTypes } from "./modifier-type";
|
||||
import { Command } from "#app/ui/command-ui-handler.js";
|
||||
import { Species } from "#enums/species";
|
||||
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { Abilities } from "#app/enums/abilities.js";
|
||||
@ -707,16 +708,16 @@ export class PokemonBaseStatModifier extends PokemonHeldItemModifier {
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier used for held items, specifically Eviolite, that apply
|
||||
* {@linkcode Stat} boost(s) using a multiplier if the holder can evolve.
|
||||
* Modifier used for held items that apply {@linkcode Stat} boost(s)
|
||||
* using a multiplier.
|
||||
* @extends PokemonHeldItemModifier
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class EvolutionStatBoosterModifier extends PokemonHeldItemModifier {
|
||||
export class StatBoosterModifier extends PokemonHeldItemModifier {
|
||||
/** The stats that the held item boosts */
|
||||
private stats: Stat[];
|
||||
protected stats: Stat[];
|
||||
/** The multiplier used to increase the relevant stat(s) */
|
||||
private multiplier: number;
|
||||
protected multiplier: number;
|
||||
|
||||
constructor(type: ModifierType, pokemonId: integer, stats: Stat[], multiplier: number, stackCount?: integer) {
|
||||
super(type, pokemonId, stackCount);
|
||||
@ -726,7 +727,7 @@ export class EvolutionStatBoosterModifier extends PokemonHeldItemModifier {
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new EvolutionStatBoosterModifier(this.type, this.pokemonId, this.stats, this.multiplier, this.stackCount);
|
||||
return new StatBoosterModifier(this.type, this.pokemonId, this.stats, this.multiplier, this.stackCount);
|
||||
}
|
||||
|
||||
getArgs(): any[] {
|
||||
@ -734,7 +735,14 @@ export class EvolutionStatBoosterModifier extends PokemonHeldItemModifier {
|
||||
}
|
||||
|
||||
matchType(modifier: Modifier): boolean {
|
||||
return modifier instanceof EvolutionStatBoosterModifier;
|
||||
if (modifier instanceof StatBoosterModifier) {
|
||||
const modifierInstance = modifier as StatBoosterModifier;
|
||||
if ((modifierInstance.multiplier === this.multiplier) && (modifierInstance.stats.length === this.stats.length)) {
|
||||
return modifierInstance.stats.every((e, i) => e === this.stats[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -745,7 +753,43 @@ export class EvolutionStatBoosterModifier extends PokemonHeldItemModifier {
|
||||
* @returns true if the stat could be boosted, false otherwise
|
||||
*/
|
||||
shouldApply(args: any[]): boolean {
|
||||
return this.stats.includes(args[1] as Stat);
|
||||
return super.shouldApply(args) && this.stats.includes(args[1] as Stat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boosts the incoming stat by a {@linkcode multiplier} if the stat is listed
|
||||
* in {@linkcode stats}.
|
||||
* @param args [0] {@linkcode Pokemon} N/A
|
||||
* [1] {@linkcode Stat} N/A
|
||||
* [2] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat
|
||||
* @returns true if the stat boost applies successfully, false otherwise
|
||||
* @see shouldApply
|
||||
*/
|
||||
apply(args: any[]): boolean {
|
||||
const statValue = args[2] as Utils.NumberHolder;
|
||||
|
||||
statValue.value *= this.multiplier;
|
||||
return true;
|
||||
}
|
||||
|
||||
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier used for held items, specifically Eviolite, that apply
|
||||
* {@linkcode Stat} boost(s) using a multiplier if the holder can evolve.
|
||||
* @extends StatBoosterModifier
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class EvolutionStatBoosterModifier extends StatBoosterModifier {
|
||||
clone() {
|
||||
return super.clone() as EvolutionStatBoosterModifier;
|
||||
}
|
||||
|
||||
matchType(modifier: Modifier): boolean {
|
||||
return modifier instanceof EvolutionStatBoosterModifier;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -771,15 +815,69 @@ export class EvolutionStatBoosterModifier extends PokemonHeldItemModifier {
|
||||
return true;
|
||||
} else if (isUnevolved) {
|
||||
// Full boost applied if holder is unfused and unevolved or, if fused, both parts of fusion are unevolved
|
||||
statValue.value *= this.multiplier;
|
||||
return true;
|
||||
return super.apply(args);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier used for held items that apply {@linkcode Stat} boost(s) using a
|
||||
* multiplier if the holder is of a specific {@linkcode Species}.
|
||||
* @extends StatBoosterModifier
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class SpeciesStatBoosterModifier extends StatBoosterModifier {
|
||||
/** The species that the held item's stat boost(s) apply to */
|
||||
private species: Species[];
|
||||
|
||||
constructor(type: ModifierType, pokemonId: integer, stats: Stat[], multiplier: number, species: Species[], stackCount?: integer) {
|
||||
super(type, pokemonId, stats, multiplier, stackCount);
|
||||
|
||||
this.species = species;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new SpeciesStatBoosterModifier(this.type, this.pokemonId, this.stats, this.multiplier, this.species, this.stackCount);
|
||||
}
|
||||
|
||||
getArgs(): any[] {
|
||||
return [ ...super.getArgs(), this.species ];
|
||||
}
|
||||
|
||||
matchType(modifier: Modifier): boolean {
|
||||
if (modifier instanceof SpeciesStatBoosterModifier) {
|
||||
const modifierInstance = modifier as SpeciesStatBoosterModifier;
|
||||
if (modifierInstance.species.length === this.species.length) {
|
||||
return super.matchType(modifier) && modifierInstance.species.every((e, i) => e === this.species[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getMaxHeldItemCount(_pokemon: Pokemon): integer {
|
||||
return 1;
|
||||
/**
|
||||
* Checks if the incoming stat is listed in {@linkcode stats} and if the holder's {@linkcode Species}
|
||||
* (or its fused species) is listed in {@linkcode species}.
|
||||
* @param args [0] {@linkcode Pokemon} that holds the held item
|
||||
* [1] {@linkcode Stat} being checked at the time
|
||||
* [2] {@linkcode Utils.NumberHolder} N/A
|
||||
* @returns true if the stat could be boosted, false otherwise
|
||||
*/
|
||||
shouldApply(args: any[]): boolean {
|
||||
const holder = args[0] as Pokemon;
|
||||
return super.shouldApply(args) && (this.species.includes(holder.getSpeciesForm(true).speciesId) || (holder.isFusion() && this.species.includes(holder.getFusionSpeciesForm(true).speciesId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if either parameter is included in the corresponding lists
|
||||
* @param speciesId {@linkcode Species} being checked
|
||||
* @param stat {@linkcode Stat} being checked
|
||||
* @returns true if both parameters are in {@linkcode species} and {@linkcode stats} respectively, false otherwise
|
||||
*/
|
||||
contains(speciesId: Species, stat: Stat): boolean {
|
||||
return this.species.includes(speciesId) && this.stats.includes(stat);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { PokeballCounts } from "./battle-scene";
|
||||
import { PokeballType } from "./data/pokeball";
|
||||
import { Gender } from "./data/gender";
|
||||
import { StatusEffect } from "./data/status-effect";
|
||||
import { modifierTypes } from "./modifier/modifier-type";
|
||||
import { SpeciesStatBoosterItem, modifierTypes } from "./modifier/modifier-type";
|
||||
import { VariantTier } from "./enums/variant-tiers";
|
||||
import { EggTier } from "#enums/egg-type";
|
||||
import { allSpecies } from "./data/pokemon-species"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
@ -125,11 +125,12 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0;
|
||||
* - Nature is for MINT
|
||||
* - Type is for TERA_SHARD or ATTACK_TYPE_BOOSTER (type boosting items i.e Silk Scarf)
|
||||
* - BerryType is for BERRY
|
||||
* - SpeciesStatBoosterItem is for SPECIES_STAT_BOOSTER
|
||||
*/
|
||||
interface ModifierOverride {
|
||||
name: keyof typeof modifierTypes & string,
|
||||
count?: integer
|
||||
type?: TempBattleStat|Stat|Nature|Type|BerryType
|
||||
type?: TempBattleStat|Stat|Nature|Type|BerryType|SpeciesStatBoosterItem
|
||||
}
|
||||
export const STARTING_MODIFIER_OVERRIDE: Array<ModifierOverride> = [];
|
||||
export const OPP_MODIFIER_OVERRIDE: Array<ModifierOverride> = [];
|
||||
|
@ -26,7 +26,7 @@ import { Gender } from "./data/gender";
|
||||
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
|
||||
import { TempBattleStat } from "./data/temp-battle-stat";
|
||||
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag";
|
||||
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr } from "./data/ability";
|
||||
import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability";
|
||||
import { Unlockables, getUnlockableName } from "./system/unlockables";
|
||||
import { getBiomeKey } from "./field/arena";
|
||||
import { BattleType, BattlerIndex, TurnCommand } from "./battle";
|
||||
@ -2878,6 +2878,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const hitCount = new Utils.IntegerHolder(1);
|
||||
// Assume single target for multi hit
|
||||
applyMoveAttrs(MultiHitAttr, user, this.getTarget(), move, hitCount);
|
||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, targets.length, hitCount, new Utils.IntegerHolder(0));
|
||||
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
|
||||
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
|
||||
}
|
||||
@ -2885,13 +2886,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
|
||||
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
|
||||
const activeTargets = targets.map(t => t.isActive(true));
|
||||
if (!activeTargets.length || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) {
|
||||
user.turnData.hitCount = 1;
|
||||
user.turnData.hitsLeft = 1;
|
||||
this.stopMultiHit();
|
||||
if (activeTargets.length) {
|
||||
this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!"));
|
||||
moveHistoryEntry.result = MoveResult.MISS;
|
||||
@ -2900,6 +2899,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
moveHistoryEntry.result = MoveResult.FAIL;
|
||||
}
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
return this.end();
|
||||
}
|
||||
|
||||
@ -2909,8 +2909,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => {
|
||||
for (const target of targets) {
|
||||
if (!targetHitChecks[target.getBattlerIndex()]) {
|
||||
user.turnData.hitCount = 1;
|
||||
user.turnData.hitsLeft = 1;
|
||||
this.stopMultiHit(target);
|
||||
this.scene.queueMessage(getPokemonMessage(user, "'s\nattack missed!"));
|
||||
if (moveHistoryEntry.result === MoveResult.PENDING) {
|
||||
moveHistoryEntry.result = MoveResult.MISS;
|
||||
@ -2921,25 +2920,33 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType));
|
||||
|
||||
const firstHit = moveHistoryEntry.result !== MoveResult.SUCCESS;
|
||||
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
||||
|
||||
if (firstHit) {
|
||||
user.pushMoveHistory(moveHistoryEntry);
|
||||
}
|
||||
|
||||
moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||
|
||||
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
|
||||
|
||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
|
||||
|
||||
if (lastHit) {
|
||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||
}
|
||||
|
||||
applyAttrs.push(new Promise(resolve => {
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit),
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit),
|
||||
user, target, move).then(() => {
|
||||
if (hitResult !== HitResult.FAIL) {
|
||||
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget(), move));
|
||||
// Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present
|
||||
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit), user, target, move)).then(() => {
|
||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => {
|
||||
if (hitResult !== HitResult.NO_EFFECT) {
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit), user, target, this.move.getMove()).then(() => {
|
||||
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
|
||||
if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
|
||||
const flinched = new Utils.BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
@ -2947,7 +2954,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
||||
}
|
||||
}
|
||||
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit),
|
||||
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit),
|
||||
user, target, this.move.getMove()).then(() => {
|
||||
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||
@ -2974,14 +2981,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
});
|
||||
}));
|
||||
}
|
||||
// Trigger effect which should only apply one time after all targeted effects have already applied
|
||||
const postTarget = applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET,
|
||||
user, null, move);
|
||||
// Trigger effect which should only apply one time on the last hit after all targeted effects have already applied
|
||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ?
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
|
||||
null;
|
||||
|
||||
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
||||
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
||||
} else { // Otherwise, push a new asynchronous move effect
|
||||
applyAttrs.push(postTarget);
|
||||
if (!!postTarget) {
|
||||
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
||||
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
||||
} else { // Otherwise, push a new asynchronous move effect
|
||||
applyAttrs.push(postTarget);
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled(applyAttrs).then(() => this.end());
|
||||
@ -3104,6 +3114,28 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return this.getTargets().find(() => true);
|
||||
}
|
||||
|
||||
removeTarget(target: Pokemon): void {
|
||||
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
|
||||
if (targetIndex !== -1) {
|
||||
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
|
||||
}
|
||||
}
|
||||
|
||||
stopMultiHit(target?: Pokemon): void {
|
||||
/** If given a specific target, remove the target from subsequent strikes */
|
||||
if (target) {
|
||||
this.removeTarget(target);
|
||||
}
|
||||
/**
|
||||
* If no target specified, or the specified target was the last of this move's
|
||||
* targets, completely cancel all subsequent strikes.
|
||||
*/
|
||||
if (!target || this.targets.length === 0 ) {
|
||||
this.getUserPokemon().turnData.hitCount = 1;
|
||||
this.getUserPokemon().turnData.hitsLeft = 1;
|
||||
}
|
||||
}
|
||||
|
||||
getNewHitPhase() {
|
||||
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ export default class PokemonData {
|
||||
public fusionLuck: integer;
|
||||
|
||||
public boss: boolean;
|
||||
public bossSegments?: integer;
|
||||
|
||||
public summonData: PokemonSummonData;
|
||||
|
||||
@ -96,6 +97,7 @@ export default class PokemonData {
|
||||
|
||||
if (!forHistory) {
|
||||
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
|
||||
this.bossSegments = source.bossSegments;
|
||||
}
|
||||
|
||||
if (sourcePokemon) {
|
||||
|
@ -6,6 +6,7 @@ import { TurnEndPhase } from "#app/phases";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Species } from "#app/enums/species.js";
|
||||
|
||||
describe("Abilities - Dry Skin", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -26,6 +27,9 @@ describe("Abilities - Dry Skin", () => {
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DRY_SKIN);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.CHARMANDER);
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||
vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.CHANDELURE);
|
||||
});
|
||||
|
||||
it("during sunlight, lose 1/8 of maximum health at the end of each turn", async () => {
|
||||
@ -84,6 +88,7 @@ describe("Abilities - Dry Skin", () => {
|
||||
expect(enemy).not.toBe(undefined);
|
||||
|
||||
// first turn
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockReturnValue(0); // this makes moves always deal 85% damage
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.EMBER));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
const fireDamageTakenWithDrySkin = enemy.getMaxHp() - enemy.hp;
|
||||
|
650
src/test/abilities/parental_bond.test.ts
Normal file
@ -0,0 +1,650 @@
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import GameManager from "../utils/gameManager";
|
||||
import * as Overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { CommandPhase, DamagePhase, MoveEffectPhase, MoveEndPhase, TurnEndPhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
import { Type } from "#app/data/type.js";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
import { StatusEffect } from "#app/data/status-effect.js";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Abilities - Parental Bond", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.PARENTAL_BOND);
|
||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"ability should add second strike to attack move",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
let enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockReturnValue(15);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
const firstStrikeDamage = enemyStartingHp - enemyPokemon.hp;
|
||||
enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
const secondStrikeDamage = enemyStartingHp - enemyPokemon.hp;
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(secondStrikeDamage).toBe(Math.ceil(0.25 * firstStrikeDamage));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should apply secondary effects to both strikes",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.POWER_UP_PUNCH]);
|
||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.AMOONGUSS);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.POWER_UP_PUNCH));
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to Status moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.BABY_DOLL_EYES]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BABY_DOLL_EYES));
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(enemyPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to multi-hit moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DOUBLE_HIT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.DOUBLE_HIT));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to self-sacrifice moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SELF_DESTRUCT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SELF_DESTRUCT));
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to Rollout",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ROLLOUT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.ROLLOUT));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply multiplier to fixed-damage moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.DRAGON_RAGE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE));
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp - 80);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply multiplier to counter moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.COUNTER]);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE,Moves.TACKLE,Moves.TACKLE,Moves.TACKLE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const playerStartingHp = leadPokemon.hp;
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.COUNTER));
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
const playerDamage = playerStartingHp - leadPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp - 4*playerDamage);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to multi-target moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.EARTHQUAKE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.PIDGEOT]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
expect(playerPokemon.length).toBe(2);
|
||||
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
expect(enemyPokemon.length).toBe(2);
|
||||
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.EARTHQUAKE));
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
playerPokemon.forEach(p => expect(p.turnData.hitCount).toBe(1));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should apply to multi-target moves when hitting only one target",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.EARTHQUAKE]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should only trigger post-target move effects once",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.MIND_BLOWN]);
|
||||
|
||||
await game.startBattle([Species.PIDGEOT]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MIND_BLOWN));
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
// This test will time out if the user faints
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(Math.floor(leadPokemon.getMaxHp()/2));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Burn Up only removes type after second strike with this ability",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.BURN_UP]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BURN_UP));
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.hp).toBeGreaterThan(0);
|
||||
expect(leadPokemon.isOfType(Type.FIRE)).toBe(true);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(leadPokemon.isOfType(Type.FIRE)).toBe(false);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Moves boosted by this ability and Multi-Lens should strike 4 times",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(4);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Super Fang boosted by this ability and Multi-Lens should strike twice",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SUPER_FANG]);
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SUPER_FANG));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(Math.ceil(enemyStartingHp * 0.25));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Seismic Toss boosted by this ability and Multi-Lens should strike twice",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SEISMIC_TOSS]);
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SEISMIC_TOSS));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp - 200);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Hyper Beam boosted by this ability should strike twice, then recharge",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.HYPER_BEAM]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.HYPER_BEAM));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.RECHARGING)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
/** TODO: Fix TRAPPED tag lapsing incorrectly, then run this test */
|
||||
test.skip(
|
||||
"Anchor Shot boosted by this ability should only trap the target after the second hit",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ANCHOR_SHOT]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.ANCHOR_SHOT));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); // Passes
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Passes
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); // Fails :(
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Smack Down boosted by this ability should only ground the target after the second hit",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SMACK_DOWN]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SMACK_DOWN));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeUndefined();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.IGNORE_FLYING)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"U-turn boosted by this ability should strike twice before forcing a switch",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.U_TURN]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.U_TURN));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
|
||||
// This will cause this test to time out if the switch was forced on the first hit.
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"Wake-Up Slap boosted by this ability should only wake up the target after the second hit",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WAKE_UP_SLAP]);
|
||||
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.SLEEP);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.WAKE_UP_SLAP));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
|
||||
expect(leadPokemon.turnData.hitCount).toBe(2);
|
||||
expect(enemyPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(enemyPokemon.status?.effect).toBeUndefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not cause user to hit into King's Shield more than once",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE]);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.KINGS_SHIELD,Moves.KINGS_SHIELD,Moves.KINGS_SHIELD,Moves.KINGS_SHIELD]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not cause user to hit into Storm Drain more than once",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WATER_GUN]);
|
||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.STORM_DRAIN);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
expect(enemyPokemon).not.toBe(undefined);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.WATER_GUN));
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
expect(enemyPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability should not apply to multi-target moves with Multi-Lens",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false);
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.EARTHQUAKE, Moves.SPLASH]);
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS", count: 1}]);
|
||||
|
||||
await game.startBattle([Species.CHARIZARD, Species.PIDGEOT]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
expect(playerPokemon.length).toBe(2);
|
||||
playerPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
expect(enemyPokemon.length).toBe(2);
|
||||
enemyPokemon.forEach(p => expect(p).not.toBe(undefined));
|
||||
|
||||
const enemyStartingHp = enemyPokemon.map(p => p.hp);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.EARTHQUAKE));
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockReturnValue(15);
|
||||
|
||||
await game.phaseInterceptor.to(DamagePhase);
|
||||
const enemyFirstHitDamage = enemyStartingHp.map((hp, i) => hp - enemyPokemon[i].hp);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase, false);
|
||||
|
||||
enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2*enemyFirstHitDamage[i]));
|
||||
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
@ -1,16 +1,13 @@
|
||||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import {Abilities} from "#enums/abilities";
|
||||
import {Species} from "#enums/species";
|
||||
import {EnemyCommandPhase, TitlePhase, TurnEndPhase, TurnStartPhase,
|
||||
} from "#app/phases";
|
||||
import * as Overrides from "#app/overrides";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Species } from "#enums/species";
|
||||
import { FaintPhase } from "#app/phases";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Stat } from "#app/data/pokemon-stat";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import { allAbilities, BypassSpeedChanceAbAttr } from "#app/data/ability";
|
||||
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
describe("Abilities - Quick Draw", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -28,90 +25,66 @@ describe("Abilities - Quick Draw", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(
|
||||
Abilities.QUICK_DRAW
|
||||
);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(
|
||||
Species.RATTATA
|
||||
);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([
|
||||
Moves.TACKLE,
|
||||
Moves.TACKLE,
|
||||
Moves.TACKLE,
|
||||
Moves.TACKLE,
|
||||
]);
|
||||
vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.QUICK_DRAW);
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TAIL_WHIP]);
|
||||
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
|
||||
|
||||
vi.spyOn(
|
||||
allAbilities[Abilities.QUICK_DRAW].getAttrs(BypassSpeedChanceAbAttr)[0],
|
||||
"chance","get"
|
||||
).mockReturnValue(100);
|
||||
vi.spyOn(allAbilities[Abilities.QUICK_DRAW].getAttrs(BypassSpeedChanceAbAttr)[0], "chance", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
it("makes pokemon going first in its priority bracket", async() => {
|
||||
test("makes pokemon going first in its priority bracket", async () => {
|
||||
await game.startBattle([Species.SLOWBRO]);
|
||||
|
||||
const pokemon = game.scene.getParty()[0];
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
pokemon.stats[Stat.SPD] = 50;
|
||||
enemy.stats[Stat.SPD] = 150;
|
||||
pokemon.hp = 1;
|
||||
enemy.hp = 1;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||
await game.phaseInterceptor.to(FaintPhase, false);
|
||||
|
||||
await game.phaseInterceptor.run(EnemyCommandPhase);
|
||||
await game.phaseInterceptor.run(TurnStartPhase);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(pokemon.battleData.abilityRevealed).toBe(true);
|
||||
expect(pokemon.isFainted()).toBe(false);
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
expect(pokemon.battleData.abilitiesApplied).contain(Abilities.QUICK_DRAW);
|
||||
}, 20000);
|
||||
|
||||
it("does not triggered by non damage moves", async () => {
|
||||
test("does not triggered by non damage moves", async () => {
|
||||
await game.startBattle([Species.SLOWBRO]);
|
||||
|
||||
const pokemon = game.scene.getParty()[0];
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
pokemon.stats[Stat.SPD] = 50;
|
||||
enemy.stats[Stat.SPD] = 150;
|
||||
pokemon.hp = 1;
|
||||
enemy.hp = 1;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TOXIC));
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TAIL_WHIP));
|
||||
await game.phaseInterceptor.to(FaintPhase, false);
|
||||
|
||||
await game.phaseInterceptor.run(EnemyCommandPhase);
|
||||
await game.phaseInterceptor.run(TurnStartPhase);
|
||||
await game.phaseInterceptor.to(TitlePhase);
|
||||
|
||||
expect(pokemon.battleData.abilityRevealed).not.toBe(true);
|
||||
expect(pokemon.isFainted()).toBe(true);
|
||||
expect(enemy.isFainted()).toBe(false);
|
||||
expect(pokemon.battleData.abilitiesApplied).not.contain(Abilities.QUICK_DRAW);
|
||||
}, 20000);
|
||||
|
||||
it("does not increase priority", async () => {
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([
|
||||
Moves.EXTREME_SPEED,
|
||||
Moves.EXTREME_SPEED,
|
||||
Moves.EXTREME_SPEED,
|
||||
Moves.EXTREME_SPEED,
|
||||
]);
|
||||
test("does not increase priority", async () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.EXTREME_SPEED));
|
||||
|
||||
await game.startBattle([Species.SLOWBRO]);
|
||||
|
||||
const pokemon = game.scene.getParty()[0];
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
pokemon.stats[Stat.SPD] = 50;
|
||||
enemy.stats[Stat.SPD] = 150;
|
||||
pokemon.hp = 1;
|
||||
enemy.hp = 1;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.TACKLE));
|
||||
await game.phaseInterceptor.to(FaintPhase, false);
|
||||
|
||||
await game.phaseInterceptor.run(EnemyCommandPhase);
|
||||
await game.phaseInterceptor.run(TurnStartPhase);
|
||||
await game.phaseInterceptor.to(TitlePhase);
|
||||
|
||||
expect(pokemon.battleData.abilityRevealed).toBe(true);
|
||||
expect(pokemon.isFainted()).toBe(true);
|
||||
expect(enemy.isFainted()).toBe(false);
|
||||
expect(pokemon.battleData.abilitiesApplied).contain(Abilities.QUICK_DRAW);
|
||||
}, 20000);
|
||||
});
|
||||
|
200
src/test/items/light_ball.test.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phase from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#app/data/pokemon-stat";
|
||||
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import * as Utils from "#app/utils";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
|
||||
describe("Items - Light Ball", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phase.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("LIGHT_BALL activates in battle correctly", async() => {
|
||||
vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{ name: "SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
// Checking consoe log to make sure Light Ball is applied when getBattleStat (with the appropriate stat) is called
|
||||
partyMember.getBattleStat(Stat.DEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
|
||||
|
||||
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPDEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.ATK);
|
||||
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPATK);
|
||||
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPD);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.LIGHT_BALL.name"), "");
|
||||
});
|
||||
|
||||
it("LIGHT_BALL held by PIKACHU", async() => {
|
||||
await game.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
const spAtkStat = partyMember.getStat(Stat.SPATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue);
|
||||
const spAtkValue = new Utils.NumberHolder(spAtkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["LIGHT_BALL"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("LIGHT_BALL held by fused PIKACHU (base)", async() => {
|
||||
await game.startBattle([
|
||||
Species.PIKACHU,
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
const spAtkStat = partyMember.getStat(Stat.SPATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue);
|
||||
const spAtkValue = new Utils.NumberHolder(spAtkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["LIGHT_BALL"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("LIGHT_BALL held by fused PIKACHU (part)", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK,
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
const spAtkStat = partyMember.getStat(Stat.SPATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue);
|
||||
const spAtkValue = new Utils.NumberHolder(spAtkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["LIGHT_BALL"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("LIGHT_BALL not held by PIKACHU", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
const spAtkStat = partyMember.getStat(Stat.SPATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, atkValue);
|
||||
const spAtkValue = new Utils.NumberHolder(spAtkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPDEF, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["LIGHT_BALL"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
}, 20000);
|
||||
});
|
176
src/test/items/metal_powder.test.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phase from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#app/data/pokemon-stat";
|
||||
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import * as Utils from "#app/utils";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
|
||||
describe("Items - Metal Powder", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phase.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("METAL_POWDER activates in battle correctly", async() => {
|
||||
vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{ name: "SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
// Checking consoe log to make sure Metal Powder is applied when getBattleStat (with the appropriate stat) is called
|
||||
partyMember.getBattleStat(Stat.DEF);
|
||||
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
|
||||
|
||||
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPDEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.ATK);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPATK);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPD);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.METAL_POWDER.name"), "");
|
||||
});
|
||||
|
||||
it("METAL_POWDER held by DITTO", async() => {
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const defStat = partyMember.getStat(Stat.DEF);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const defValue = new Utils.NumberHolder(defStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["METAL_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("METAL_POWDER held by fused DITTO (base)", async() => {
|
||||
await game.startBattle([
|
||||
Species.DITTO,
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const defStat = partyMember.getStat(Stat.DEF);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const defValue = new Utils.NumberHolder(defStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["METAL_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("METAL_POWDER held by fused DITTO (part)", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK,
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const defStat = partyMember.getStat(Stat.DEF);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const defValue = new Utils.NumberHolder(defStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["METAL_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("METAL_POWDER not held by DITTO", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const defStat = partyMember.getStat(Stat.DEF);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const defValue = new Utils.NumberHolder(defStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["METAL_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
}, 20000);
|
||||
});
|
176
src/test/items/quick_powder.test.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phase from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#app/data/pokemon-stat";
|
||||
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import * as Utils from "#app/utils";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
|
||||
describe("Items - Quick Powder", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phase.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("QUICK_POWDER activates in battle correctly", async() => {
|
||||
vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
// Checking consoe log to make sure Quick Powder is applied when getBattleStat (with the appropriate stat) is called
|
||||
partyMember.getBattleStat(Stat.DEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
|
||||
|
||||
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPDEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.ATK);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPATK);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPD);
|
||||
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.QUICK_POWDER.name"), "");
|
||||
});
|
||||
|
||||
it("QUICK_POWDER held by DITTO", async() => {
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const spdStat = partyMember.getStat(Stat.SPD);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const spdValue = new Utils.NumberHolder(spdStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["QUICK_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("QUICK_POWDER held by fused DITTO (base)", async() => {
|
||||
await game.startBattle([
|
||||
Species.DITTO,
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const spdStat = partyMember.getStat(Stat.SPD);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const spdValue = new Utils.NumberHolder(spdStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["QUICK_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("QUICK_POWDER held by fused DITTO (part)", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK,
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const spdStat = partyMember.getStat(Stat.SPD);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const spdValue = new Utils.NumberHolder(spdStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["QUICK_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("QUICK_POWDER not held by DITTO", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const spdStat = partyMember.getStat(Stat.SPD);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const spdValue = new Utils.NumberHolder(spdStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["QUICK_POWDER"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
}, 20000);
|
||||
});
|
228
src/test/items/thick_club.test.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Phase from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import * as overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#app/data/pokemon-stat";
|
||||
import { SpeciesStatBoosterModifier } from "#app/modifier/modifier";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import * as Utils from "#app/utils";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
|
||||
describe("Items - Thick Club", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phase.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("THICK_CLUB activates in battle correctly", async() => {
|
||||
vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
Species.CUBONE
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
// Checking consoe log to make sure Thick Club is applied when getBattleStat (with the appropriate stat) is called
|
||||
partyMember.getBattleStat(Stat.DEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
|
||||
|
||||
// Printing dummy console messages along the way so subsequent checks don't pass because of the first
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPDEF);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.ATK);
|
||||
expect(consoleSpy).toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPATK);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
|
||||
|
||||
console.log("");
|
||||
|
||||
partyMember.getBattleStat(Stat.SPD);
|
||||
expect(consoleSpy).not.toHaveBeenLastCalledWith("Applied", i18next.t("modifierType:SpeciesBoosterItem.THICK_CLUB.name"), "");
|
||||
});
|
||||
|
||||
it("THICK_CLUB held by CUBONE", async() => {
|
||||
await game.startBattle([
|
||||
Species.CUBONE
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["THICK_CLUB"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("THICK_CLUB held by MAROWAK", async() => {
|
||||
await game.startBattle([
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["THICK_CLUB"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("THICK_CLUB held by ALOLA_MAROWAK", async() => {
|
||||
await game.startBattle([
|
||||
Species.ALOLA_MAROWAK
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["THICK_CLUB"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("THICK_CLUB held by fused CUBONE line (base)", async() => {
|
||||
// Randomly choose from the Cubone line
|
||||
const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ];
|
||||
const randSpecies = Utils.randInt(species.length);
|
||||
|
||||
await game.startBattle([
|
||||
species[randSpecies],
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["THICK_CLUB"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("THICK_CLUB held by fused CUBONE line (part)", async() => {
|
||||
// Randomly choose from the Cubone line
|
||||
const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ];
|
||||
const randSpecies = Utils.randInt(species.length);
|
||||
|
||||
await game.startBattle([
|
||||
Species.PIKACHU,
|
||||
species[randSpecies]
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
const ally = game.scene.getParty()[1];
|
||||
|
||||
// Fuse party members (taken from PlayerPokemon.fuse(...) function)
|
||||
partyMember.fusionSpecies = ally.species;
|
||||
partyMember.fusionFormIndex = ally.formIndex;
|
||||
partyMember.fusionAbilityIndex = ally.abilityIndex;
|
||||
partyMember.fusionShiny = ally.shiny;
|
||||
partyMember.fusionVariant = ally.variant;
|
||||
partyMember.fusionGender = ally.gender;
|
||||
partyMember.fusionLuck = ally.luck;
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["THICK_CLUB"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
|
||||
it("THICK_CLUB not held by CUBONE", async() => {
|
||||
await game.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
const partyMember = game.scene.getParty()[0];
|
||||
|
||||
const atkStat = partyMember.getStat(Stat.ATK);
|
||||
|
||||
// Making sure modifier is not applied without holding item
|
||||
const atkValue = new Utils.NumberHolder(atkStat);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
partyMember.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType(null, ["THICK_CLUB"]).newModifier(partyMember), true);
|
||||
partyMember.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
}, 20000);
|
||||
});
|