mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-11-25 08:16:04 +00:00
[Move] Implement Stockpile, Spit Up, Swallow (#2960)
* feat: Implement Stockpile, Spit Up, Swallow * chore: Minor, likely unnecessary null checks * feat: Localization * Undo non-English localizations (unsure if they went through proper channels) * ko localization from @EnochG1 Co-authored-by: Enoch <enoch.jwsong@gmail.com> * linting fix * add tests, tiny (non-functional) tweaks * Remove unnecessary cast * Update src/data/move.ts (oops) * remove some unnecessary comments, rename something for clarity --------- Co-authored-by: Enoch <enoch.jwsong@gmail.com>
This commit is contained in:
parent
ef5e0d4c24
commit
0e5fd80431
@ -1,5 +1,5 @@
|
||||
import { CommonAnim, CommonBattleAnim } from "./battle-anims";
|
||||
import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases";
|
||||
import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangeCallback, StatChangePhase } from "../phases";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
|
||||
import { Stat, getStatName } from "./pokemon-stat";
|
||||
@ -1583,6 +1583,94 @@ export class IceFaceTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Battler tag enabling the Stockpile mechanic. This tag handles:
|
||||
* - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy).
|
||||
*
|
||||
* - Stat changes on adding a stack. Adding a stockpile stack attempts to raise the pokemon's DEF and SPDEF by +1.
|
||||
*
|
||||
* - Stat changes on removal of (all) stacks.
|
||||
* - Removing stacks decreases DEF and SPDEF, independently, by one stage for each stack that successfully changed
|
||||
* the stat when added.
|
||||
*/
|
||||
export class StockpilingTag extends BattlerTag {
|
||||
public stockpiledCount: number = 0;
|
||||
public statChangeCounts: { [BattleStat.DEF]: number; [BattleStat.SPDEF]: number } = {
|
||||
[BattleStat.DEF]: 0,
|
||||
[BattleStat.SPDEF]: 0
|
||||
};
|
||||
|
||||
constructor(sourceMove: Moves = Moves.NONE) {
|
||||
super(BattlerTagType.STOCKPILING, BattlerTagLapseType.CUSTOM, 1, sourceMove);
|
||||
}
|
||||
|
||||
private onStatsChanged: StatChangeCallback = (_, statsChanged, statChanges) => {
|
||||
const defChange = statChanges[statsChanged.indexOf(BattleStat.DEF)] ?? 0;
|
||||
const spDefChange = statChanges[statsChanged.indexOf(BattleStat.SPDEF)] ?? 0;
|
||||
|
||||
if (defChange) {
|
||||
this.statChangeCounts[BattleStat.DEF]++;
|
||||
}
|
||||
|
||||
if (spDefChange) {
|
||||
this.statChangeCounts[BattleStat.SPDEF]++;
|
||||
}
|
||||
};
|
||||
|
||||
loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.stockpiledCount = source.stockpiledCount || 0;
|
||||
this.statChangeCounts = {
|
||||
[BattleStat.DEF]: source.statChangeCounts?.[BattleStat.DEF] || 0,
|
||||
[BattleStat.SPDEF]: source.statChangeCounts?.[BattleStat.SPDEF] || 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a stockpile stack to a pokemon, up to a maximum of 3 stacks. Note that onOverlap defers to this method.
|
||||
*
|
||||
* If a stack is added, a message is displayed and the pokemon's DEF and SPDEF are increased by 1.
|
||||
* For each stat, an internal counter is incremented (by 1) if the stat was successfully changed.
|
||||
*/
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
if (this.stockpiledCount < 3) {
|
||||
this.stockpiledCount++;
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsStockpilingOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
stockpiledCount: this.stockpiledCount
|
||||
}));
|
||||
|
||||
// Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes.
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(
|
||||
pokemon.scene, pokemon.getBattlerIndex(), true,
|
||||
[BattleStat.SPDEF, BattleStat.DEF], 1, true, false, true, this.onStatsChanged
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
onOverlap(pokemon: Pokemon): void {
|
||||
this.onAdd(pokemon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removing the tag removes all stacks, and the pokemon's DEF and SPDEF are decreased by
|
||||
* one stage for each stack which had successfully changed that particular stat during onAdd.
|
||||
*/
|
||||
onRemove(pokemon: Pokemon): void {
|
||||
const defChange = this.statChangeCounts[BattleStat.DEF];
|
||||
const spDefChange = this.statChangeCounts[BattleStat.SPDEF];
|
||||
|
||||
if (defChange) {
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF], -defChange, true, false, true));
|
||||
}
|
||||
|
||||
if (spDefChange) {
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.SPDEF], -spDefChange, true, false, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourceMove: Moves, sourceId: integer): BattlerTag {
|
||||
switch (tagType) {
|
||||
case BattlerTagType.RECHARGING:
|
||||
@ -1704,6 +1792,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc
|
||||
return new DestinyBondTag(sourceMove, sourceId);
|
||||
case BattlerTagType.ICE_FACE:
|
||||
return new IceFaceTag(sourceMove);
|
||||
case BattlerTagType.STOCKPILING:
|
||||
return new StockpilingTag(sourceMove);
|
||||
case BattlerTagType.OCTOLOCK:
|
||||
return new OctolockTag(sourceId);
|
||||
case BattlerTagType.NONE:
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
|
||||
import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases";
|
||||
import { BattleStat, getBattleStatName } from "./battle-stat";
|
||||
import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, TypeBoostTag } from "./battler-tags";
|
||||
import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, TypeBoostTag } from "./battler-tags";
|
||||
import { getPokemonMessage, getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
|
||||
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect";
|
||||
@ -3295,6 +3295,62 @@ export class WaterShurikenPowerAttr extends VariablePowerAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to calculate the power of attacks that scale with Stockpile stacks (i.e. Spit Up).
|
||||
*/
|
||||
export class SpitUpPowerAttr extends VariablePowerAttr {
|
||||
private multiplier: number = 0;
|
||||
|
||||
constructor(multiplier: number) {
|
||||
super();
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
|
||||
if (stockpilingTag?.stockpiledCount > 0) {
|
||||
const power = args[0] as Utils.IntegerHolder;
|
||||
power.value = this.multiplier * stockpilingTag.stockpiledCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used to apply Swallow's healing, which scales with Stockpile stacks.
|
||||
* Does NOT remove stockpiled stacks.
|
||||
*/
|
||||
export class SwallowHealAttr extends HealAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
|
||||
if (stockpilingTag?.stockpiledCount > 0) {
|
||||
const stockpiled = stockpilingTag.stockpiledCount;
|
||||
let healRatio: number;
|
||||
|
||||
if (stockpiled === 1) {
|
||||
healRatio = 0.25;
|
||||
} else if (stockpiled === 2) {
|
||||
healRatio = 0.50;
|
||||
} else { // stockpiled >= 3
|
||||
healRatio = 1.00;
|
||||
}
|
||||
|
||||
if (healRatio) {
|
||||
this.addHealPhase(user, healRatio);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const hasStockpileStacksCondition: MoveConditionFunc = (user) => user.getTag(StockpilingTag)?.stockpiledCount > 0;
|
||||
|
||||
/**
|
||||
* Attribute used for multi-hit moves that increase power in increments of the
|
||||
* move's base power for each hit, namely Triple Kick and Triple Axel.
|
||||
@ -6536,12 +6592,17 @@ export function initMoves() {
|
||||
.target(MoveTarget.RANDOM_NEAR_ENEMY)
|
||||
.partial(),
|
||||
new SelfStatusMove(Moves.STOCKPILE, Type.NORMAL, -1, 20, -1, 0, 3)
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.SPIT_UP, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
||||
.unimplemented(),
|
||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||
new AttackMove(Moves.SPIT_UP, Type.NORMAL, MoveCategory.SPECIAL, -1, -1, 10, -1, 0, 3)
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SpitUpPowerAttr, 100)
|
||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.STOCKPILING], true),
|
||||
new SelfStatusMove(Moves.SWALLOW, Type.NORMAL, -1, 10, -1, 0, 3)
|
||||
.triageMove()
|
||||
.unimplemented(),
|
||||
.condition(hasStockpileStacksCondition)
|
||||
.attr(SwallowHealAttr)
|
||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.STOCKPILING], true)
|
||||
.triageMove(),
|
||||
new AttackMove(Moves.HEAT_WAVE, Type.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||
|
@ -61,6 +61,7 @@ export enum BattlerTagType {
|
||||
DESTINY_BOND = "DESTINY_BOND",
|
||||
CENTER_OF_ATTENTION = "CENTER_OF_ATTENTION",
|
||||
ICE_FACE = "ICE_FACE",
|
||||
STOCKPILING = "STOCKPILING",
|
||||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT"
|
||||
}
|
||||
|
@ -2232,13 +2232,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @overload */
|
||||
getTag(tagType: BattlerTagType): BattlerTag;
|
||||
|
||||
/** @overload */
|
||||
getTag<T extends BattlerTag>(tagType: Constructor<T>): T;
|
||||
|
||||
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag {
|
||||
if (!this.summonData) {
|
||||
return null;
|
||||
}
|
||||
return typeof(tagType) === "string"
|
||||
? this.summonData.tags.find(t => t.tagType === tagType)
|
||||
: this.summonData.tags.find(t => t instanceof tagType);
|
||||
return tagType instanceof Function
|
||||
? this.summonData.tags.find(t => t instanceof tagType)
|
||||
: this.summonData.tags.find(t => t.tagType === tagType);
|
||||
}
|
||||
|
||||
findTag(tagFilter: ((tag: BattlerTag) => boolean)) {
|
||||
@ -2248,15 +2254,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return this.summonData.tags.find(t => tagFilter(t));
|
||||
}
|
||||
|
||||
getTags(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag[] {
|
||||
if (!this.summonData) {
|
||||
return [];
|
||||
}
|
||||
return typeof(tagType) === "string"
|
||||
? this.summonData.tags.filter(t => t.tagType === tagType)
|
||||
: this.summonData.tags.filter(t => t instanceof tagType);
|
||||
}
|
||||
|
||||
findTags(tagFilter: ((tag: BattlerTag) => boolean)): BattlerTag[] {
|
||||
if (!this.summonData) {
|
||||
return [];
|
||||
|
@ -154,5 +154,6 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredOnAdd": "{{pokemonNameWithAffix}} wurde eingepökelt!",
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} wurde durch {{moveName}} verletzt!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} nimmt einen Teil seiner KP und legt einen Fluch auf {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} wurde durch den Fluch verletzt!"
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} wurde durch den Fluch verletzt!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -155,4 +155,5 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -154,5 +154,6 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredOnAdd": "{{pokemonNameWithAffix}} is being salt cured!",
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!"
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -154,5 +154,6 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredOnAdd": "{{pokemonNameWithAffix}}\nest couvert de sel !",
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} est blessé\npar la capacité {{moveName}} !",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} sacrifie des PV\net lance une malédiction sur {{pokemonName}} !",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} est touché par la malédiction !"
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} est touché par la malédiction !",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -154,5 +154,6 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredOnAdd": "{{pokemonNameWithAffix}} è stato messo sotto sale!",
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} viene colpito da {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} ha sacrificato metà dei suoi PS per\nlanciare una maledizione su {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} subisce la maledizione!"
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} subisce la maledizione!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -155,4 +155,5 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}}[[는]] 소금절이의\n데미지를 입고 있다.",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}}[[는]] 자신의 체력을 깎아서\n{{pokemonName}}에게 저주를 걸었다!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}[[는]]\n저주받고 있다!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!",
|
||||
} as const;
|
||||
|
@ -155,4 +155,5 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} foi ferido pelo {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cortou seus PS pela metade e amaldiçoou {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} foi ferido pelo Curse!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
} as const;
|
||||
|
@ -146,5 +146,6 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredOnAdd": "{{pokemonNameWithAffix}}\n陷入了盐腌状态!",
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}}\n受到了{{moveName}}的伤害!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}}削减了自己的体力,\n并诅咒了{{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}\n正受到诅咒!"
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}\n正受到诅咒!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -144,4 +144,5 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} 受到了{{moveName}}的傷害!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}}削減了自己的體力,並詛咒了{{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}正受到詛咒!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
} as const;
|
||||
|
@ -3224,6 +3224,8 @@ export class ShowAbilityPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
export type StatChangeCallback = (target: Pokemon, changed: BattleStat[], relativeChanges: number[]) => void;
|
||||
|
||||
export class StatChangePhase extends PokemonPhase {
|
||||
private stats: BattleStat[];
|
||||
private selfTarget: boolean;
|
||||
@ -3231,8 +3233,10 @@ export class StatChangePhase extends PokemonPhase {
|
||||
private showMessage: boolean;
|
||||
private ignoreAbilities: boolean;
|
||||
private canBeCopied: boolean;
|
||||
private onChange: StatChangeCallback;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true) {
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatChangeCallback = null) {
|
||||
super(scene, battlerIndex);
|
||||
|
||||
this.selfTarget = selfTarget;
|
||||
@ -3241,6 +3245,7 @@ export class StatChangePhase extends PokemonPhase {
|
||||
this.showMessage = showMessage;
|
||||
this.ignoreAbilities = ignoreAbilities;
|
||||
this.canBeCopied = canBeCopied;
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -3282,6 +3287,8 @@ export class StatChangePhase extends PokemonPhase {
|
||||
const battleStats = this.getPokemon().summonData.battleStats;
|
||||
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]);
|
||||
|
||||
this.onChange?.(this.getPokemon(), filteredStats, relLevels);
|
||||
|
||||
const end = () => {
|
||||
if (this.showMessage) {
|
||||
const messages = this.getStatChangeMessages(filteredStats, levels.value, relLevels);
|
||||
|
161
src/test/battlerTags/stockpiling.test.ts
Normal file
161
src/test/battlerTags/stockpiling.test.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Pokemon, { PokemonSummonData } from "#app/field/pokemon.js";
|
||||
import BattleScene from "#app/battle-scene.js";
|
||||
import { StockpilingTag } from "#app/data/battler-tags.js";
|
||||
import { StatChangePhase } from "#app/phases.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
import * as messages from "#app/messages.js";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(messages, "getPokemonNameWithAffix").mockImplementation(() => "");
|
||||
});
|
||||
|
||||
describe("BattlerTag - StockpilingTag", () => {
|
||||
describe("onAdd", () => {
|
||||
it("unshifts a StatChangePhase with expected stat changes on add", { timeout: 10000 }, async () => {
|
||||
const mockPokemon = {
|
||||
scene: vi.mocked(new BattleScene()) as BattleScene,
|
||||
getBattlerIndex: () => 0,
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
|
||||
|
||||
const subject = new StockpilingTag(1);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(1);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
|
||||
|
||||
(phase as StatChangePhase)["onChange"](mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]);
|
||||
});
|
||||
|
||||
subject.onAdd(mockPokemon);
|
||||
|
||||
expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("unshifts a StatChangePhase with expected stat changes on add (one stat maxed)", { timeout: 10000 }, async () => {
|
||||
const mockPokemon = {
|
||||
scene: new BattleScene(),
|
||||
summonData: new PokemonSummonData(),
|
||||
getBattlerIndex: () => 0,
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
|
||||
|
||||
mockPokemon.summonData.battleStats[BattleStat.DEF] = 6;
|
||||
mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 5;
|
||||
|
||||
const subject = new StockpilingTag(1);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(1);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
|
||||
|
||||
(phase as StatChangePhase)["onChange"](mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]);
|
||||
});
|
||||
|
||||
subject.onAdd(mockPokemon);
|
||||
|
||||
expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onOverlap", () => {
|
||||
it("unshifts a StatChangePhase with expected stat changes on overlap", { timeout: 10000 }, async () => {
|
||||
const mockPokemon = {
|
||||
scene: new BattleScene(),
|
||||
getBattlerIndex: () => 0,
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
|
||||
|
||||
const subject = new StockpilingTag(1);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementation(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(1);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
|
||||
|
||||
(phase as StatChangePhase)["onChange"](mockPokemon, [BattleStat.DEF, BattleStat.SPDEF], [1, 1]);
|
||||
});
|
||||
|
||||
subject.onOverlap(mockPokemon);
|
||||
|
||||
expect(mockPokemon.scene.unshiftPhase).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stack limit, stat tracking, and removal", () => {
|
||||
it("can be added up to three times, even when one stat does not change", { timeout: 10000 }, async () => {
|
||||
const mockPokemon = {
|
||||
scene: new BattleScene(),
|
||||
summonData: new PokemonSummonData(),
|
||||
getBattlerIndex: () => 0,
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockImplementation(() => {});
|
||||
|
||||
mockPokemon.summonData.battleStats[BattleStat.DEF] = 5;
|
||||
mockPokemon.summonData.battleStats[BattleStat.SPDEF] = 4;
|
||||
|
||||
const subject = new StockpilingTag(1);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(1);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
|
||||
|
||||
// def doesn't change
|
||||
(phase as StatChangePhase)["onChange"](mockPokemon, [BattleStat.SPDEF], [1]);
|
||||
});
|
||||
|
||||
subject.onAdd(mockPokemon);
|
||||
expect(subject.stockpiledCount).toBe(1);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(1);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
|
||||
|
||||
// def doesn't change
|
||||
(phase as StatChangePhase)["onChange"](mockPokemon, [BattleStat.SPDEF], [1]);
|
||||
});
|
||||
|
||||
subject.onOverlap(mockPokemon);
|
||||
expect(subject.stockpiledCount).toBe(2);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(1);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.DEF, BattleStat.SPDEF]));
|
||||
|
||||
// neither stat changes, stack count should still increase
|
||||
});
|
||||
|
||||
subject.onOverlap(mockPokemon);
|
||||
expect(subject.stockpiledCount).toBe(3);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
|
||||
throw new Error("Should not be called a fourth time");
|
||||
});
|
||||
|
||||
// fourth stack should not be applied
|
||||
subject.onOverlap(mockPokemon);
|
||||
expect(subject.stockpiledCount).toBe(3);
|
||||
expect(subject.statChangeCounts).toMatchObject({ [BattleStat.DEF]: 0, [BattleStat.SPDEF]: 2 });
|
||||
|
||||
// removing tag should reverse stat changes
|
||||
vi.spyOn(mockPokemon.scene, "unshiftPhase").mockImplementationOnce(phase => {
|
||||
expect(phase).toBeInstanceOf(StatChangePhase);
|
||||
expect((phase as StatChangePhase)["levels"]).toEqual(-2);
|
||||
expect((phase as StatChangePhase)["stats"]).toEqual(expect.arrayContaining([BattleStat.SPDEF]));
|
||||
});
|
||||
|
||||
subject.onRemove(mockPokemon);
|
||||
expect(mockPokemon.scene.unshiftPhase).toHaveBeenCalledOnce(); // note that re-spying each add/overlap has been refreshing call count
|
||||
});
|
||||
});
|
||||
});
|
201
src/test/moves/spit_up.test.ts
Normal file
201
src/test/moves/spit_up.test.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import overrides from "#app/overrides";
|
||||
import { MovePhase, TurnInitPhase } from "#app/phases";
|
||||
import { BattleStat } from "#app/data/battle-stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StockpilingTag } from "#app/data/battler-tags.js";
|
||||
import { MoveResult, TurnMove } from "#app/field/pokemon.js";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
|
||||
describe("Moves - Spit Up", () => {
|
||||
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, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
|
||||
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPIT_UP, Moves.SPIT_UP, Moves.SPIT_UP, Moves.SPIT_UP]);
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
});
|
||||
|
||||
describe("consumes all stockpile stacks to deal damage (scaling with stacks)", () => {
|
||||
it("1 stack -> 100 power", { timeout: 10000 }, async () => {
|
||||
const stacksToSetup = 1;
|
||||
const expectedPower = 100;
|
||||
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
|
||||
|
||||
vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("2 stacks -> 200 power", { timeout: 10000 }, async () => {
|
||||
const stacksToSetup = 2;
|
||||
const expectedPower = 200;
|
||||
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
|
||||
|
||||
vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("3 stacks -> 300 power", { timeout: 10000 }, async () => {
|
||||
const stacksToSetup = 3;
|
||||
const expectedPower = 300;
|
||||
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
|
||||
|
||||
vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveReturnedWith(expectedPower);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("fails without stacks", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeUndefined();
|
||||
|
||||
vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.FAIL });
|
||||
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("restores stat boosts granted by stacks", () => {
|
||||
it("decreases stats based on stored values (both boosts equal)", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
|
||||
vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(MovePhase);
|
||||
|
||||
expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
|
||||
expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1);
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS });
|
||||
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
|
||||
|
||||
expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(0);
|
||||
expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("decreases stats based on stored values (different boosts)", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
|
||||
// for the sake of simplicity (and because other tests cover the setup), set boost amounts directly
|
||||
stockpilingTag.statChangeCounts = {
|
||||
[BattleStat.DEF]: -1,
|
||||
[BattleStat.SPDEF]: 2,
|
||||
};
|
||||
|
||||
expect(stockpilingTag.statChangeCounts).toMatchObject({
|
||||
[BattleStat.DEF]: -1,
|
||||
[BattleStat.SPDEF]: 2,
|
||||
});
|
||||
|
||||
vi.spyOn(allMoves[Moves.SPIT_UP], "calculateBattlePower");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS });
|
||||
|
||||
expect(allMoves[Moves.SPIT_UP].calculateBattlePower).toHaveBeenCalledOnce();
|
||||
|
||||
expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
|
||||
expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(-2);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
116
src/test/moves/stockpile.test.ts
Normal file
116
src/test/moves/stockpile.test.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import overrides from "#app/overrides";
|
||||
import { CommandPhase, TurnInitPhase } from "#app/phases";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import { BattleStat } from "#app/data/battle-stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StockpilingTag } from "#app/data/battler-tags.js";
|
||||
import { MoveResult, TurnMove } from "#app/field/pokemon.js";
|
||||
|
||||
describe("Moves - Stockpile", () => {
|
||||
describe("integration tests", () => {
|
||||
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, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
|
||||
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.STOCKPILE, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
});
|
||||
|
||||
it("Gains a stockpile stack and increases DEF and SPDEF by 1 on each use, fails at max stacks (3)", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const user = game.scene.getPlayerPokemon();
|
||||
|
||||
// Unfortunately, Stockpile stacks are not directly queryable (i.e. there is no pokemon.getStockpileStacks()),
|
||||
// we just have to know that they're implemented as a BattlerTag.
|
||||
|
||||
expect(user.getTag(StockpilingTag)).toBeUndefined();
|
||||
expect(user.summonData.battleStats[BattleStat.DEF]).toBe(0);
|
||||
expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
|
||||
|
||||
// use Stockpile four times
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (i !== 0) {
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
}
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.STOCKPILE));
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
const def = user.summonData.battleStats[BattleStat.DEF];
|
||||
const spdef = user.summonData.battleStats[BattleStat.SPDEF];
|
||||
|
||||
if (i < 3) { // first three uses should behave normally
|
||||
expect(def).toBe(i + 1);
|
||||
expect(spdef).toBe(i + 1);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(i + 1);
|
||||
|
||||
} else { // fourth should have failed
|
||||
expect(def).toBe(3);
|
||||
expect(spdef).toBe(3);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(3);
|
||||
expect(user.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ result: MoveResult.FAIL, move: Moves.STOCKPILE });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("Gains a stockpile stack even if DEF and SPDEF are at +6", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const user = game.scene.getPlayerPokemon();
|
||||
|
||||
user.summonData.battleStats[BattleStat.DEF] = 6;
|
||||
user.summonData.battleStats[BattleStat.SPDEF] = 6;
|
||||
|
||||
expect(user.getTag(StockpilingTag)).toBeUndefined();
|
||||
expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6);
|
||||
expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.STOCKPILE));
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
const stockpilingTag = user.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(1);
|
||||
expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6);
|
||||
expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
|
||||
|
||||
// do it again, just for good measure
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.STOCKPILE));
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
const stockpilingTagAgain = user.getTag(StockpilingTag);
|
||||
expect(stockpilingTagAgain).toBeDefined();
|
||||
expect(stockpilingTagAgain.stockpiledCount).toBe(2);
|
||||
expect(user.summonData.battleStats[BattleStat.DEF]).toBe(6);
|
||||
expect(user.summonData.battleStats[BattleStat.SPDEF]).toBe(6);
|
||||
});
|
||||
});
|
||||
});
|
197
src/test/moves/swallow.test.ts
Normal file
197
src/test/moves/swallow.test.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import overrides from "#app/overrides";
|
||||
import { MovePhase, TurnInitPhase } from "#app/phases";
|
||||
import { BattleStat } from "#app/data/battle-stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StockpilingTag } from "#app/data/battler-tags.js";
|
||||
import { MoveResult, TurnMove } from "#app/field/pokemon.js";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
|
||||
describe("Moves - Swallow", () => {
|
||||
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, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
|
||||
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SWALLOW, Moves.SWALLOW, Moves.SWALLOW, Moves.SWALLOW]);
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
});
|
||||
|
||||
describe("consumes all stockpile stacks to heal (scaling with stacks)", () => {
|
||||
it("1 stack -> 25% heal", { timeout: 10000 }, async () => {
|
||||
const stacksToSetup = 1;
|
||||
const expectedHeal = 25;
|
||||
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
vi.spyOn(pokemon, "getMaxHp").mockReturnValue(100);
|
||||
pokemon["hp"] = 1;
|
||||
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
|
||||
|
||||
vi.spyOn(pokemon, "heal");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.heal).toHaveBeenCalledOnce();
|
||||
expect(pokemon.heal).toHaveReturnedWith(expectedHeal);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("2 stacks -> 50% heal", { timeout: 10000 }, async () => {
|
||||
const stacksToSetup = 2;
|
||||
const expectedHeal = 50;
|
||||
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
vi.spyOn(pokemon, "getMaxHp").mockReturnValue(100);
|
||||
pokemon["hp"] = 1;
|
||||
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
|
||||
|
||||
vi.spyOn(pokemon, "heal");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.heal).toHaveBeenCalledOnce();
|
||||
expect(pokemon.heal).toHaveReturnedWith(expectedHeal);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("3 stacks -> 100% heal", { timeout: 10000 }, async () => {
|
||||
const stacksToSetup = 3;
|
||||
const expectedHeal = 100;
|
||||
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
vi.spyOn(pokemon, "getMaxHp").mockReturnValue(100);
|
||||
pokemon["hp"] = 0.0001;
|
||||
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup);
|
||||
|
||||
vi.spyOn(pokemon, "heal");
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.heal).toHaveBeenCalledOnce();
|
||||
expect(pokemon.heal).toHaveReturnedWith(expect.closeTo(expectedHeal));
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("fails without stacks", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeUndefined();
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
describe("restores stat boosts granted by stacks", () => {
|
||||
it("decreases stats based on stored values (both boosts equal)", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(MovePhase);
|
||||
|
||||
expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
|
||||
expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1);
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS });
|
||||
|
||||
expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(0);
|
||||
expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("decreases stats based on stored values (different boosts)", { timeout: 10000 }, async () => {
|
||||
await game.startBattle([Species.ABOMASNOW]);
|
||||
|
||||
const pokemon = game.scene.getPlayerPokemon();
|
||||
pokemon.addTag(BattlerTagType.STOCKPILING);
|
||||
|
||||
const stockpilingTag = pokemon.getTag(StockpilingTag);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
|
||||
// for the sake of simplicity (and because other tests cover the setup), set boost amounts directly
|
||||
stockpilingTag.statChangeCounts = {
|
||||
[BattleStat.DEF]: -1,
|
||||
[BattleStat.SPDEF]: 2,
|
||||
};
|
||||
|
||||
expect(stockpilingTag.statChangeCounts).toMatchObject({
|
||||
[BattleStat.DEF]: -1,
|
||||
[BattleStat.SPDEF]: 2,
|
||||
});
|
||||
|
||||
game.doAttack(0);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS });
|
||||
|
||||
expect(pokemon.summonData.battleStats[BattleStat.DEF]).toBe(1);
|
||||
expect(pokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(-2);
|
||||
|
||||
expect(pokemon.getTag(StockpilingTag)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user