mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-11-25 16:26:25 +00:00
[Item/Balance] Overhaul Lapsing Modifiers (#4032)
* Refactor Lapsing Modifiers, Lerp Hue of Count * Fix Unit Tests * Add Documentation to `hslToHex` Function * Change Descriptions for New Behavior * Add Documentation to Lapsing Modifiers * Add Unit Tests for Lures * Update Unit Tests for X Items and Lures * Update Boilerplate Error Message * Update Boilerplate Docs
This commit is contained in:
parent
ba212945de
commit
7288350d45
@ -4,7 +4,8 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* This script creates a test boilerplate file for a move or ability.
|
||||
* @param {string} type - The type of test to create. Either "move" or "ability".
|
||||
* @param {string} type - The type of test to create. Either "move", "ability",
|
||||
* or "item".
|
||||
* @param {string} fileName - The name of the file to create.
|
||||
* @example npm run create-test move tackle
|
||||
*/
|
||||
@ -19,7 +20,7 @@ const type = args[0]; // "move" or "ability"
|
||||
let fileName = args[1]; // The file name
|
||||
|
||||
if (!type || !fileName) {
|
||||
console.error('Please provide both a type ("move" or "ability") and a file name.');
|
||||
console.error('Please provide both a type ("move", "ability", or "item") and a file name.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -40,8 +41,11 @@ if (type === 'move') {
|
||||
} else if (type === 'ability') {
|
||||
dir = path.join(__dirname, 'src', 'test', 'abilities');
|
||||
description = `Abilities - ${formattedName}`;
|
||||
} else if (type === "item") {
|
||||
dir = path.join(__dirname, 'src', 'test', 'items');
|
||||
description = `Items - ${formattedName}`;
|
||||
} else {
|
||||
console.error('Invalid type. Please use "move" or "ability".');
|
||||
console.error('Invalid type. Please use "move", "ability", or "item".');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -47,10 +47,14 @@
|
||||
"description": "Changes a Pokémon's nature to {{natureName}} and permanently unlocks the nature for the starter."
|
||||
},
|
||||
"DoubleBattleChanceBoosterModifierType": {
|
||||
"description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles."
|
||||
"description": "Quadruples the chance of an encounter being a double battle for up to {{battleCount}} battles."
|
||||
},
|
||||
"TempStatStageBoosterModifierType": {
|
||||
"description": "Increases the {{stat}} of all party members by 1 stage for 5 battles."
|
||||
"description": "Increases the {{stat}} of all party members by {{amount}} for up to 5 battles.",
|
||||
"extra": {
|
||||
"stage": "1 stage",
|
||||
"percentage": "30%"
|
||||
}
|
||||
},
|
||||
"AttackTypeBoosterModifierType": {
|
||||
"description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%."
|
||||
|
@ -433,37 +433,44 @@ export class RememberMoveModifierType extends PokemonModifierType {
|
||||
}
|
||||
|
||||
export class DoubleBattleChanceBoosterModifierType extends ModifierType {
|
||||
public battleCount: integer;
|
||||
private maxBattles: number;
|
||||
|
||||
constructor(localeKey: string, iconImage: string, battleCount: integer) {
|
||||
super(localeKey, iconImage, (_type, _args) => new Modifiers.DoubleBattleChanceBoosterModifier(this, this.battleCount), "lure");
|
||||
constructor(localeKey: string, iconImage: string, maxBattles: number) {
|
||||
super(localeKey, iconImage, (_type, _args) => new Modifiers.DoubleBattleChanceBoosterModifier(this, maxBattles), "lure");
|
||||
|
||||
this.battleCount = battleCount;
|
||||
this.maxBattles = maxBattles;
|
||||
}
|
||||
|
||||
getDescription(scene: BattleScene): string {
|
||||
return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { battleCount: this.battleCount });
|
||||
getDescription(_scene: BattleScene): string {
|
||||
return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", {
|
||||
battleCount: this.maxBattles
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType {
|
||||
private stat: TempBattleStat;
|
||||
private key: string;
|
||||
private nameKey: string;
|
||||
private quantityKey: string;
|
||||
|
||||
constructor(stat: TempBattleStat) {
|
||||
const key = TempStatStageBoosterModifierTypeGenerator.items[stat];
|
||||
super("", key, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat));
|
||||
const nameKey = TempStatStageBoosterModifierTypeGenerator.items[stat];
|
||||
super("", nameKey, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat, 5));
|
||||
|
||||
this.stat = stat;
|
||||
this.key = key;
|
||||
this.nameKey = nameKey;
|
||||
this.quantityKey = (stat !== Stat.ACC) ? "percentage" : "stage";
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return i18next.t(`modifierType:TempStatStageBoosterItem.${this.key}`);
|
||||
return i18next.t(`modifierType:TempStatStageBoosterItem.${this.nameKey}`);
|
||||
}
|
||||
|
||||
getDescription(_scene: BattleScene): string {
|
||||
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) });
|
||||
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
|
||||
stat: i18next.t(getStatKey(this.stat)),
|
||||
amount: i18next.t(`modifierType:ModifierType.TempStatStageBoosterModifierType.extra.${this.quantityKey}`)
|
||||
});
|
||||
}
|
||||
|
||||
getPregenArgs(): any[] {
|
||||
@ -1348,9 +1355,9 @@ export const modifierTypes = {
|
||||
SUPER_REPEL: () => new DoubleBattleChanceBoosterModifierType('Super Repel', 10),
|
||||
MAX_REPEL: () => new DoubleBattleChanceBoosterModifierType('Max Repel', 25),*/
|
||||
|
||||
LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 5),
|
||||
SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 10),
|
||||
MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 25),
|
||||
LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 10),
|
||||
SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 15),
|
||||
MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 30),
|
||||
|
||||
SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(),
|
||||
|
||||
@ -1358,9 +1365,12 @@ export const modifierTypes = {
|
||||
|
||||
DIRE_HIT: () => new class extends ModifierType {
|
||||
getDescription(_scene: BattleScene): string {
|
||||
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises") });
|
||||
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
|
||||
stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"),
|
||||
amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage")
|
||||
});
|
||||
}
|
||||
}("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type)),
|
||||
}("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type, 5)),
|
||||
|
||||
BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(),
|
||||
|
||||
|
@ -292,70 +292,131 @@ export class AddVoucherModifier extends ConsumableModifier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier used for party-wide or passive items that start an initial
|
||||
* {@linkcode battleCount} equal to {@linkcode maxBattles} that, for every
|
||||
* battle, decrements. Typically, when {@linkcode battleCount} reaches 0, the
|
||||
* modifier will be removed. If a modifier of the same type is to be added, it
|
||||
* will reset {@linkcode battleCount} back to {@linkcode maxBattles} of the
|
||||
* existing modifier instead of adding that modifier directly.
|
||||
* @extends PersistentModifier
|
||||
* @abstract
|
||||
* @see {@linkcode add}
|
||||
*/
|
||||
export abstract class LapsingPersistentModifier extends PersistentModifier {
|
||||
protected battlesLeft: integer;
|
||||
/** The maximum amount of battles the modifier will exist for */
|
||||
private maxBattles: number;
|
||||
/** The current amount of battles the modifier will exist for */
|
||||
private battleCount: number;
|
||||
|
||||
constructor(type: ModifierTypes.ModifierType, battlesLeft?: integer, stackCount?: integer) {
|
||||
constructor(type: ModifierTypes.ModifierType, maxBattles: number, battleCount?: number, stackCount?: integer) {
|
||||
super(type, stackCount);
|
||||
|
||||
this.battlesLeft = battlesLeft!; // TODO: is this bang correct?
|
||||
this.maxBattles = maxBattles;
|
||||
this.battleCount = battleCount ?? this.maxBattles;
|
||||
}
|
||||
|
||||
lapse(args: any[]): boolean {
|
||||
return !!--this.battlesLeft;
|
||||
/**
|
||||
* Goes through existing modifiers for any that match the selected modifier,
|
||||
* which will then either add it to the existing modifiers if none were found
|
||||
* or, if one was found, it will refresh {@linkcode battleCount}.
|
||||
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
|
||||
* @param _virtual N/A
|
||||
* @param _scene N/A
|
||||
* @returns true if the modifier was successfully added or applied, false otherwise
|
||||
*/
|
||||
add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean {
|
||||
for (const modifier of modifiers) {
|
||||
if (this.match(modifier)) {
|
||||
const modifierInstance = modifier as LapsingPersistentModifier;
|
||||
if (modifierInstance.getBattleCount() < modifierInstance.getMaxBattles()) {
|
||||
modifierInstance.resetBattleCount();
|
||||
scene.playSound("se/restore");
|
||||
return true;
|
||||
}
|
||||
// should never get here
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modifiers.push(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
lapse(_args: any[]): boolean {
|
||||
this.battleCount--;
|
||||
return this.battleCount > 0;
|
||||
}
|
||||
|
||||
getIcon(scene: BattleScene): Phaser.GameObjects.Container {
|
||||
const container = super.getIcon(scene);
|
||||
|
||||
const battleCountText = addTextObject(scene, 27, 0, this.battlesLeft.toString(), TextStyle.PARTY, { fontSize: "66px", color: "#f89890" });
|
||||
// Linear interpolation on hue
|
||||
const hue = Math.floor(120 * (this.battleCount / this.maxBattles) + 5);
|
||||
|
||||
// Generates the color hex code with a constant saturation and lightness but varying hue
|
||||
const typeHex = Utils.hslToHex(hue, 0.50, 0.90);
|
||||
const strokeHex = Utils.hslToHex(hue, 0.70, 0.30);
|
||||
|
||||
const battleCountText = addTextObject(scene, 27, 0, this.battleCount.toString(), TextStyle.PARTY, { fontSize: "66px", color: typeHex });
|
||||
battleCountText.setShadow(0, 0);
|
||||
battleCountText.setStroke("#984038", 16);
|
||||
battleCountText.setStroke(strokeHex, 16);
|
||||
battleCountText.setOrigin(1, 0);
|
||||
container.add(battleCountText);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
getBattlesLeft(): integer {
|
||||
return this.battlesLeft;
|
||||
getBattleCount(): number {
|
||||
return this.battleCount;
|
||||
}
|
||||
|
||||
getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number {
|
||||
return 99;
|
||||
}
|
||||
}
|
||||
|
||||
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
|
||||
constructor(type: ModifierTypes.DoubleBattleChanceBoosterModifierType, battlesLeft: integer, stackCount?: integer) {
|
||||
super(type, battlesLeft, stackCount);
|
||||
resetBattleCount(): void {
|
||||
this.battleCount = this.maxBattles;
|
||||
}
|
||||
|
||||
match(modifier: Modifier): boolean {
|
||||
if (modifier instanceof DoubleBattleChanceBoosterModifier) {
|
||||
// Check type id to not match different tiers of lures
|
||||
return modifier.type.id === this.type.id && modifier.battlesLeft === this.battlesLeft;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
clone(): DoubleBattleChanceBoosterModifier {
|
||||
return new DoubleBattleChanceBoosterModifier(this.type as ModifierTypes.DoubleBattleChanceBoosterModifierType, this.battlesLeft, this.stackCount);
|
||||
getMaxBattles(): number {
|
||||
return this.maxBattles;
|
||||
}
|
||||
|
||||
getArgs(): any[] {
|
||||
return [ this.battlesLeft ];
|
||||
return [ this.maxBattles, this.battleCount ];
|
||||
}
|
||||
|
||||
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier used for passive items, specifically lures, that
|
||||
* temporarily increases the chance of a double battle.
|
||||
* @extends LapsingPersistentModifier
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
|
||||
constructor(type: ModifierType, maxBattles:number, battleCount?: number, stackCount?: integer) {
|
||||
super(type, maxBattles, battleCount, stackCount);
|
||||
}
|
||||
|
||||
match(modifier: Modifier): boolean {
|
||||
return (modifier instanceof DoubleBattleChanceBoosterModifier) && (modifier.getMaxBattles() === this.getMaxBattles());
|
||||
}
|
||||
|
||||
clone(): DoubleBattleChanceBoosterModifier {
|
||||
return new DoubleBattleChanceBoosterModifier(this.type as ModifierTypes.DoubleBattleChanceBoosterModifierType, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the chance of a double battle occurring
|
||||
* @param args A single element array containing the double battle chance as a NumberHolder
|
||||
* @returns {boolean} Returns true if the modifier was applied
|
||||
* @param args [0] {@linkcode Utils.NumberHolder} for double battle chance
|
||||
* @returns true if the modifier was applied
|
||||
*/
|
||||
apply(args: any[]): boolean {
|
||||
const doubleBattleChance = args[0] as Utils.NumberHolder;
|
||||
// This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using Utils.randSeedInt
|
||||
// A double battle will initiate if the generated number is 0
|
||||
doubleBattleChance.value = Math.ceil(doubleBattleChance.value / 2);
|
||||
doubleBattleChance.value = Math.ceil(doubleBattleChance.value / 4);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -369,16 +430,18 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
|
||||
/** The stat whose stat stage multiplier will be temporarily increased */
|
||||
private stat: TempBattleStat;
|
||||
private multiplierBoost: number;
|
||||
/** The amount by which the stat stage itself or its multiplier will be increased by */
|
||||
private boost: number;
|
||||
|
||||
constructor(type: ModifierType, stat: TempBattleStat, battlesLeft?: number, stackCount?: number) {
|
||||
super(type, battlesLeft ?? 5, stackCount);
|
||||
constructor(type: ModifierType, stat: TempBattleStat, maxBattles: number, battleCount?: number, stackCount?: number) {
|
||||
super(type, maxBattles, battleCount, stackCount);
|
||||
|
||||
this.stat = stat;
|
||||
// Note that, because we want X Accuracy to maintain its original behavior,
|
||||
// it will increment as it did previously, directly to the stat stage.
|
||||
this.multiplierBoost = stat !== Stat.ACC ? 0.3 : 1;
|
||||
this.boost = (stat !== Stat.ACC) ? 0.3 : 1;
|
||||
}
|
||||
|
||||
match(modifier: Modifier): boolean {
|
||||
@ -390,11 +453,11 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new TempStatStageBoosterModifier(this.type, this.stat, this.battlesLeft, this.stackCount);
|
||||
return new TempStatStageBoosterModifier(this.type, this.stat, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
|
||||
}
|
||||
|
||||
getArgs(): any[] {
|
||||
return [ this.stat, this.battlesLeft ];
|
||||
return [ this.stat, ...super.getArgs() ];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -409,44 +472,14 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the incoming stat stage matching {@linkcode stat} by {@linkcode multiplierBoost}.
|
||||
* Increases the incoming stat stage matching {@linkcode stat} by {@linkcode boost}.
|
||||
* @param args [0] {@linkcode TempBattleStat} N/A
|
||||
* [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier
|
||||
*/
|
||||
apply(args: any[]): boolean {
|
||||
(args[1] as Utils.NumberHolder).value += this.multiplierBoost;
|
||||
(args[1] as Utils.NumberHolder).value += this.boost;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through existing modifiers for any that match the selected modifier,
|
||||
* which will then either add it to the existing modifiers if none were found
|
||||
* or, if one was found, it will refresh {@linkcode battlesLeft}.
|
||||
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
|
||||
* @param _virtual N/A
|
||||
* @param _scene N/A
|
||||
* @returns true if the modifier was successfully added or applied, false otherwise
|
||||
*/
|
||||
add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
|
||||
for (const modifier of modifiers) {
|
||||
if (this.match(modifier)) {
|
||||
const modifierInstance = modifier as TempStatStageBoosterModifier;
|
||||
if (modifierInstance.getBattlesLeft() < 5) {
|
||||
modifierInstance.battlesLeft = 5;
|
||||
return true;
|
||||
}
|
||||
// should never get here
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modifiers.push(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -456,12 +489,12 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class TempCritBoosterModifier extends LapsingPersistentModifier {
|
||||
constructor(type: ModifierType, battlesLeft?: integer, stackCount?: number) {
|
||||
super(type, battlesLeft || 5, stackCount);
|
||||
constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) {
|
||||
super(type, maxBattles, battleCount, stackCount);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new TempCritBoosterModifier(this.type, this.stackCount);
|
||||
return new TempCritBoosterModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
|
||||
}
|
||||
|
||||
match(modifier: Modifier): boolean {
|
||||
@ -486,36 +519,6 @@ export class TempCritBoosterModifier extends LapsingPersistentModifier {
|
||||
(args[0] as Utils.NumberHolder).value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through existing modifiers for any that match the selected modifier,
|
||||
* which will then either add it to the existing modifiers if none were found
|
||||
* or, if one was found, it will refresh {@linkcode battlesLeft}.
|
||||
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
|
||||
* @param _virtual N/A
|
||||
* @param _scene N/A
|
||||
* @returns true if the modifier was successfully added or applied, false otherwise
|
||||
*/
|
||||
add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
|
||||
for (const modifier of modifiers) {
|
||||
if (this.match(modifier)) {
|
||||
const modifierInstance = modifier as TempCritBoosterModifier;
|
||||
if (modifierInstance.getBattlesLeft() < 5) {
|
||||
modifierInstance.battlesLeft = 5;
|
||||
return true;
|
||||
}
|
||||
// should never get here
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modifiers.push(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class MapModifier extends PersistentModifier {
|
||||
|
@ -72,7 +72,7 @@ describe("Items - Dire Hit", () => {
|
||||
await game.phaseInterceptor.to(BattleEndPhase);
|
||||
|
||||
const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier;
|
||||
expect(modifier.getBattlesLeft()).toBe(4);
|
||||
expect(modifier.getBattleCount()).toBe(4);
|
||||
|
||||
// Forced DIRE_HIT to spawn in the first slot with override
|
||||
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
|
||||
@ -90,7 +90,7 @@ describe("Items - Dire Hit", () => {
|
||||
for (const m of game.scene.modifiers) {
|
||||
if (m instanceof TempCritBoosterModifier) {
|
||||
count++;
|
||||
expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5);
|
||||
expect((m as TempCritBoosterModifier).getBattleCount()).toBe(5);
|
||||
}
|
||||
}
|
||||
expect(count).toBe(1);
|
||||
|
105
src/test/items/double_battle_chance_booster.test.ts
Normal file
105
src/test/items/double_battle_chance_booster.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { Species } from "#app/enums/species.js";
|
||||
import { DoubleBattleChanceBoosterModifier } from "#app/modifier/modifier";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
import { ShopCursorTarget } from "#app/enums/shop-cursor-target.js";
|
||||
import { Mode } from "#app/ui/ui.js";
|
||||
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler.js";
|
||||
import { Button } from "#app/enums/buttons.js";
|
||||
|
||||
describe("Items - Double Battle Chance Boosters", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
});
|
||||
|
||||
it("should guarantee double battle with 2 unique tiers", async () => {
|
||||
game.override
|
||||
.startingModifier([
|
||||
{ name: "LURE" },
|
||||
{ name: "SUPER_LURE" }
|
||||
])
|
||||
.startingWave(2);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
expect(game.scene.getEnemyField().length).toBe(2);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should guarantee double boss battle with 3 unique tiers", async () => {
|
||||
game.override
|
||||
.startingModifier([
|
||||
{ name: "LURE" },
|
||||
{ name: "SUPER_LURE" },
|
||||
{ name: "MAX_LURE" }
|
||||
])
|
||||
.startingWave(10);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyField = game.scene.getEnemyField();
|
||||
|
||||
expect(enemyField.length).toBe(2);
|
||||
expect(enemyField[0].isBoss()).toBe(true);
|
||||
expect(enemyField[1].isBoss()).toBe(true);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should renew how many battles are left of existing booster when picking up new booster of same tier", async() => {
|
||||
game.override
|
||||
.startingModifier([{ name: "LURE" }])
|
||||
.itemRewards([{ name: "LURE" }])
|
||||
.moveset(SPLASH_ONLY)
|
||||
.startingLevel(200);
|
||||
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.doKillOpponents();
|
||||
|
||||
await game.phaseInterceptor.to("BattleEndPhase");
|
||||
|
||||
const modifier = game.scene.findModifier(m => m instanceof DoubleBattleChanceBoosterModifier) as DoubleBattleChanceBoosterModifier;
|
||||
expect(modifier.getBattleCount()).toBe(9);
|
||||
|
||||
// Forced LURE to spawn in the first slot with override
|
||||
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
|
||||
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
|
||||
// Traverse to first modifier slot
|
||||
handler.setCursor(0);
|
||||
handler.setRowCursor(ShopCursorTarget.REWARDS);
|
||||
handler.processInput(Button.ACTION);
|
||||
}, () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("NewBattlePhase"), true);
|
||||
|
||||
await game.phaseInterceptor.to("TurnInitPhase");
|
||||
|
||||
// Making sure only one booster is in the modifier list even after picking up another
|
||||
let count = 0;
|
||||
for (const m of game.scene.modifiers) {
|
||||
if (m instanceof DoubleBattleChanceBoosterModifier) {
|
||||
count++;
|
||||
const modifierInstance = m as DoubleBattleChanceBoosterModifier;
|
||||
expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles());
|
||||
}
|
||||
}
|
||||
expect(count).toBe(1);
|
||||
}, TIMEOUT);
|
||||
});
|
@ -10,12 +10,7 @@ import { Abilities } from "#app/enums/abilities";
|
||||
import { TempStatStageBoosterModifier } from "#app/modifier/modifier";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import { Button } from "#app/enums/buttons";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
||||
import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
|
||||
|
||||
|
||||
@ -46,7 +41,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
});
|
||||
|
||||
it("should provide a x1.3 stat stage multiplier", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -56,7 +51,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
|
||||
await game.phaseInterceptor.runFrom("EnemyCommandPhase").to(TurnEndPhase);
|
||||
|
||||
expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3);
|
||||
}, 20000);
|
||||
@ -66,7 +61,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }])
|
||||
.ability(Abilities.SIMPLE);
|
||||
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -89,7 +84,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
|
||||
|
||||
it("should increase existing stat stage multiplier by 3/10 for the rest of the boosters", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -113,7 +108,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
it("should not increase past maximum stat stage multiplier", async() => {
|
||||
game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }, { name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
|
||||
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -138,7 +133,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
.startingLevel(200)
|
||||
.itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
|
||||
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -146,10 +141,10 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
|
||||
await game.doKillOpponents();
|
||||
|
||||
await game.phaseInterceptor.to(BattleEndPhase);
|
||||
await game.phaseInterceptor.to("BattleEndPhase");
|
||||
|
||||
const modifier = game.scene.findModifier(m => m instanceof TempStatStageBoosterModifier) as TempStatStageBoosterModifier;
|
||||
expect(modifier.getBattlesLeft()).toBe(4);
|
||||
expect(modifier.getBattleCount()).toBe(4);
|
||||
|
||||
// Forced X_ATTACK to spawn in the first slot with override
|
||||
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
|
||||
@ -158,16 +153,17 @@ describe("Items - Temporary Stat Stage Boosters", () => {
|
||||
handler.setCursor(0);
|
||||
handler.setRowCursor(ShopCursorTarget.REWARDS);
|
||||
handler.processInput(Button.ACTION);
|
||||
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true);
|
||||
}, () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("NewBattlePhase"), true);
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
await game.phaseInterceptor.to("TurnInitPhase");
|
||||
|
||||
// Making sure only one booster is in the modifier list even after picking up another
|
||||
let count = 0;
|
||||
for (const m of game.scene.modifiers) {
|
||||
if (m instanceof TempStatStageBoosterModifier) {
|
||||
count++;
|
||||
expect((m as TempStatStageBoosterModifier).getBattlesLeft()).toBe(5);
|
||||
const modifierInstance = m as TempStatStageBoosterModifier;
|
||||
expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles());
|
||||
}
|
||||
}
|
||||
expect(count).toBe(1);
|
||||
|
20
src/utils.ts
20
src/utils.ts
@ -455,6 +455,26 @@ export function rgbaToInt(rgba: integer[]): integer {
|
||||
return (rgba[0] << 24) + (rgba[1] << 16) + (rgba[2] << 8) + rgba[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided valid HSV values, calculates and stitches together a string of that
|
||||
* HSV color's corresponding hex code.
|
||||
*
|
||||
* Sourced from {@link https://stackoverflow.com/a/44134328}.
|
||||
* @param h Hue in degrees, must be in a range of [0, 360]
|
||||
* @param s Saturation percentage, must be in a range of [0, 1]
|
||||
* @param l Ligthness percentage, must be in a range of [0, 1]
|
||||
* @returns a string of the corresponding color hex code with a "#" prefix
|
||||
*/
|
||||
export function hslToHex(h: number, s: number, l: number): string {
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12;
|
||||
const rgb = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||
return Math.round(rgb * 255).toString(16).padStart(2, "0");
|
||||
};
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
}
|
||||
|
||||
/*This function returns true if the current lang is available for some functions
|
||||
If the lang is not in the function, it usually means that lang is going to use the default english version
|
||||
This function is used in:
|
||||
|
Loading…
Reference in New Issue
Block a user