[Item] Add Light Ball, Thick Club, Metal Powder, and Quick Powder (#1899)

* Add Light Ball, Thick Club, and Metal/Quick Powder

* Fix Typo, Account for Fusions

* Fix Errors with Fusions

* Group Items into Generator

* Refactor Generator and Type, Fix Localization (en)

* Adjust Generator, Add Override Type

* Fix Enum Import

* Localize for de, es, fr, it, ko, zh_CN, zh_TW

* Add Unit Tests

* Add Type

* Fix Items Atlas

* Change Cubone Passive Ability, Adjust Weight
This commit is contained in:
Amani H 2024-06-26 14:48:10 -04:00 committed by GitHub
parent 74a7b1bb79
commit f7f8988cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 7757 additions and 6642 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -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,

View File

@ -10,7 +10,7 @@ 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";
@ -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)) {

View File

@ -35,6 +35,7 @@ export interface ModifierTypeTranslationEntry {
export interface ModifierTypeTranslationEntries {
ModifierType: { [key: string]: ModifierTypeTranslationEntry },
SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry },
AttackTypeBoosterItem: SimpleTranslationEntries,
TempBattleStatBoosterItem: SimpleTranslationEntries,
TempBattleStatBoosterStatName: SimpleTranslationEntries,

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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 quun 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 +",

View File

@ -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 lAttacco e lAttacco Speciale." },
"THICK_CLUB": { name: "Osso spesso", description: "Strumento da dare a Cubone o Marowak. Osso duro che aumenta lAttacco." },
"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",

View File

@ -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": "디펜드업",

View File

@ -241,6 +241,8 @@ 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: {
},
TempBattleStatBoosterItem: {
"x_attack": "Ataque X",
"x_defense": "Defesa X",

View File

@ -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": "防御强化",

View File

@ -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: "防禦強化",

View File

@ -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);
@ -1369,6 +1469,7 @@ const modifierPool: ModifierPool = {
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];

View File

@ -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;
}
/**
@ -748,6 +756,42 @@ export class EvolutionStatBoosterModifier extends PokemonHeldItemModifier {
return 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;
}
/**
* Boosts the incoming stat value by a {@linkcode multiplier} if the holder
* can evolve. Note that, if the holder is a fusion, they will receive
@ -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);
}
}

View File

@ -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> = [];

View 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);
});

View 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);
});

View 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);
});

View 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);
});