From 7288350d45d53f9141a42486c69c37297b1d5285 Mon Sep 17 00:00:00 2001 From: "Amani H." <109637146+xsn34kzx@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:54:54 -0400 Subject: [PATCH] [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 --- create-test-boilerplate.js | 12 +- src/locales/en/modifier-type.json | 8 +- src/modifier/modifier-type.ts | 44 ++-- src/modifier/modifier.ts | 207 +++++++++--------- src/test/items/dire_hit.test.ts | 4 +- .../double_battle_chance_booster.test.ts | 105 +++++++++ .../items/temp_stat_stage_booster.test.ts | 28 +-- src/utils.ts | 20 ++ 8 files changed, 285 insertions(+), 143 deletions(-) create mode 100644 src/test/items/double_battle_chance_booster.test.ts diff --git a/create-test-boilerplate.js b/create-test-boilerplate.js index bf68258f321..3c53eb1125b 100644 --- a/create-test-boilerplate.js +++ b/create-test-boilerplate.js @@ -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); } @@ -98,4 +102,4 @@ describe("${description}", () => { // Write the template content to the file fs.writeFileSync(filePath, content, 'utf8'); -console.log(`File created at: ${filePath}`); \ No newline at end of file +console.log(`File created at: ${filePath}`); diff --git a/src/locales/en/modifier-type.json b/src/locales/en/modifier-type.json index f73a3dcccae..babad57b81b 100644 --- a/src/locales/en/modifier-type.json +++ b/src/locales/en/modifier-type.json @@ -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%." diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index fe586074c79..d6cfd017829 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -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(), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index f3219c8bf73..c1d58a7bf39 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -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 { diff --git a/src/test/items/dire_hit.test.ts b/src/test/items/dire_hit.test.ts index 02f7c0d06a4..4b5988294f3 100644 --- a/src/test/items/dire_hit.test.ts +++ b/src/test/items/dire_hit.test.ts @@ -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); diff --git a/src/test/items/double_battle_chance_booster.test.ts b/src/test/items/double_battle_chance_booster.test.ts new file mode 100644 index 00000000000..808d4c7ca51 --- /dev/null +++ b/src/test/items/double_battle_chance_booster.test.ts @@ -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); +}); diff --git a/src/test/items/temp_stat_stage_booster.test.ts b/src/test/items/temp_stat_stage_booster.test.ts index c81703220db..3e32fa13a04 100644 --- a/src/test/items/temp_stat_stage_booster.test.ts +++ b/src/test/items/temp_stat_stage_booster.test.ts @@ -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); diff --git a/src/utils.ts b/src/utils.ts index fd5430d7276..592981c7643 100644 --- a/src/utils.ts +++ b/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: