[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:
Amani H. 2024-09-06 22:54:54 -04:00 committed by GitHub
parent ba212945de
commit 7288350d45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 285 additions and 143 deletions

View File

@ -4,7 +4,8 @@ import { fileURLToPath } from 'url';
/** /**
* This script creates a test boilerplate file for a move or ability. * 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. * @param {string} fileName - The name of the file to create.
* @example npm run create-test move tackle * @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 let fileName = args[1]; // The file name
if (!type || !fileName) { 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); process.exit(1);
} }
@ -40,8 +41,11 @@ if (type === 'move') {
} else if (type === 'ability') { } else if (type === 'ability') {
dir = path.join(__dirname, 'src', 'test', 'abilities'); dir = path.join(__dirname, 'src', 'test', 'abilities');
description = `Abilities - ${formattedName}`; description = `Abilities - ${formattedName}`;
} else if (type === "item") {
dir = path.join(__dirname, 'src', 'test', 'items');
description = `Items - ${formattedName}`;
} else { } else {
console.error('Invalid type. Please use "move" or "ability".'); console.error('Invalid type. Please use "move", "ability", or "item".');
process.exit(1); process.exit(1);
} }

View File

@ -47,10 +47,14 @@
"description": "Changes a Pokémon's nature to {{natureName}} and permanently unlocks the nature for the starter." "description": "Changes a Pokémon's nature to {{natureName}} and permanently unlocks the nature for the starter."
}, },
"DoubleBattleChanceBoosterModifierType": { "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": { "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": { "AttackTypeBoosterModifierType": {
"description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%." "description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%."

View File

@ -433,37 +433,44 @@ export class RememberMoveModifierType extends PokemonModifierType {
} }
export class DoubleBattleChanceBoosterModifierType extends ModifierType { export class DoubleBattleChanceBoosterModifierType extends ModifierType {
public battleCount: integer; private maxBattles: number;
constructor(localeKey: string, iconImage: string, battleCount: integer) { constructor(localeKey: string, iconImage: string, maxBattles: number) {
super(localeKey, iconImage, (_type, _args) => new Modifiers.DoubleBattleChanceBoosterModifier(this, this.battleCount), "lure"); super(localeKey, iconImage, (_type, _args) => new Modifiers.DoubleBattleChanceBoosterModifier(this, maxBattles), "lure");
this.battleCount = battleCount; this.maxBattles = maxBattles;
} }
getDescription(scene: BattleScene): string { getDescription(_scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { battleCount: this.battleCount }); return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", {
battleCount: this.maxBattles
});
} }
} }
export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType { export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType {
private stat: TempBattleStat; private stat: TempBattleStat;
private key: string; private nameKey: string;
private quantityKey: string;
constructor(stat: TempBattleStat) { constructor(stat: TempBattleStat) {
const key = TempStatStageBoosterModifierTypeGenerator.items[stat]; const nameKey = TempStatStageBoosterModifierTypeGenerator.items[stat];
super("", key, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat)); super("", nameKey, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat, 5));
this.stat = stat; this.stat = stat;
this.key = key; this.nameKey = nameKey;
this.quantityKey = (stat !== Stat.ACC) ? "percentage" : "stage";
} }
get name(): string { get name(): string {
return i18next.t(`modifierType:TempStatStageBoosterItem.${this.key}`); return i18next.t(`modifierType:TempStatStageBoosterItem.${this.nameKey}`);
} }
getDescription(_scene: BattleScene): string { 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[] { getPregenArgs(): any[] {
@ -1348,9 +1355,9 @@ export const modifierTypes = {
SUPER_REPEL: () => new DoubleBattleChanceBoosterModifierType('Super Repel', 10), SUPER_REPEL: () => new DoubleBattleChanceBoosterModifierType('Super Repel', 10),
MAX_REPEL: () => new DoubleBattleChanceBoosterModifierType('Max Repel', 25),*/ MAX_REPEL: () => new DoubleBattleChanceBoosterModifierType('Max Repel', 25),*/
LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 5), LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 10),
SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 10), SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 15),
MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 25), MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 30),
SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(), SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(),
@ -1358,9 +1365,12 @@ export const modifierTypes = {
DIRE_HIT: () => new class extends ModifierType { DIRE_HIT: () => new class extends ModifierType {
getDescription(_scene: BattleScene): string { 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(), BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(),

View File

@ -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 { 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); 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 { getIcon(scene: BattleScene): Phaser.GameObjects.Container {
const container = super.getIcon(scene); 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.setShadow(0, 0);
battleCountText.setStroke("#984038", 16); battleCountText.setStroke(strokeHex, 16);
battleCountText.setOrigin(1, 0); battleCountText.setOrigin(1, 0);
container.add(battleCountText); container.add(battleCountText);
return container; return container;
} }
getBattlesLeft(): integer { getBattleCount(): number {
return this.battlesLeft; return this.battleCount;
} }
getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number { resetBattleCount(): void {
return 99; this.battleCount = this.maxBattles;
}
}
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
constructor(type: ModifierTypes.DoubleBattleChanceBoosterModifierType, battlesLeft: integer, stackCount?: integer) {
super(type, battlesLeft, stackCount);
} }
match(modifier: Modifier): boolean { getMaxBattles(): number {
if (modifier instanceof DoubleBattleChanceBoosterModifier) { return this.maxBattles;
// 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);
} }
getArgs(): any[] { 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 * Modifies the chance of a double battle occurring
* @param args A single element array containing the double battle chance as a NumberHolder * @param args [0] {@linkcode Utils.NumberHolder} for double battle chance
* @returns {boolean} Returns true if the modifier was applied * @returns true if the modifier was applied
*/ */
apply(args: any[]): boolean { apply(args: any[]): boolean {
const doubleBattleChance = args[0] as Utils.NumberHolder; 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 // 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 // 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; return true;
} }
@ -369,16 +430,18 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class TempStatStageBoosterModifier extends LapsingPersistentModifier { export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
/** The stat whose stat stage multiplier will be temporarily increased */
private stat: TempBattleStat; 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) { constructor(type: ModifierType, stat: TempBattleStat, maxBattles: number, battleCount?: number, stackCount?: number) {
super(type, battlesLeft ?? 5, stackCount); super(type, maxBattles, battleCount, stackCount);
this.stat = stat; this.stat = stat;
// Note that, because we want X Accuracy to maintain its original behavior, // Note that, because we want X Accuracy to maintain its original behavior,
// it will increment as it did previously, directly to the stat stage. // 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 { match(modifier: Modifier): boolean {
@ -390,11 +453,11 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
} }
clone() { 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[] { 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 * @param args [0] {@linkcode TempBattleStat} N/A
* [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier * [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier
*/ */
apply(args: any[]): boolean { apply(args: any[]): boolean {
(args[1] as Utils.NumberHolder).value += this.multiplierBoost; (args[1] as Utils.NumberHolder).value += this.boost;
return true; 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} * @see {@linkcode apply}
*/ */
export class TempCritBoosterModifier extends LapsingPersistentModifier { export class TempCritBoosterModifier extends LapsingPersistentModifier {
constructor(type: ModifierType, battlesLeft?: integer, stackCount?: number) { constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) {
super(type, battlesLeft || 5, stackCount); super(type, maxBattles, battleCount, stackCount);
} }
clone() { clone() {
return new TempCritBoosterModifier(this.type, this.stackCount); return new TempCritBoosterModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
} }
match(modifier: Modifier): boolean { match(modifier: Modifier): boolean {
@ -486,36 +519,6 @@ export class TempCritBoosterModifier extends LapsingPersistentModifier {
(args[0] as Utils.NumberHolder).value++; (args[0] as Utils.NumberHolder).value++;
return true; 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 { export class MapModifier extends PersistentModifier {

View File

@ -72,7 +72,7 @@ describe("Items - Dire Hit", () => {
await game.phaseInterceptor.to(BattleEndPhase); await game.phaseInterceptor.to(BattleEndPhase);
const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier; 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 // Forced DIRE_HIT to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => { game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
@ -90,7 +90,7 @@ describe("Items - Dire Hit", () => {
for (const m of game.scene.modifiers) { for (const m of game.scene.modifiers) {
if (m instanceof TempCritBoosterModifier) { if (m instanceof TempCritBoosterModifier) {
count++; count++;
expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5); expect((m as TempCritBoosterModifier).getBattleCount()).toBe(5);
} }
} }
expect(count).toBe(1); expect(count).toBe(1);

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

View File

@ -10,12 +10,7 @@ import { Abilities } from "#app/enums/abilities";
import { TempStatStageBoosterModifier } from "#app/modifier/modifier"; import { TempStatStageBoosterModifier } from "#app/modifier/modifier";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { Button } from "#app/enums/buttons"; 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 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"; 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() => { it("should provide a x1.3 stat stage multiplier", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
@ -56,7 +51,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
game.move.select(Moves.TACKLE); 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); expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3);
}, 20000); }, 20000);
@ -66,7 +61,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }]) .startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }])
.ability(Abilities.SIMPLE); .ability(Abilities.SIMPLE);
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU 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() => { 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 Species.PIKACHU
]); ]);
@ -113,7 +108,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
it("should not increase past maximum stat stage multiplier", async() => { 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 }]); 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 Species.PIKACHU
]); ]);
@ -138,7 +133,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
.startingLevel(200) .startingLevel(200)
.itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]); .itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
@ -146,10 +141,10 @@ describe("Items - Temporary Stat Stage Boosters", () => {
await game.doKillOpponents(); await game.doKillOpponents();
await game.phaseInterceptor.to(BattleEndPhase); await game.phaseInterceptor.to("BattleEndPhase");
const modifier = game.scene.findModifier(m => m instanceof TempStatStageBoosterModifier) as TempStatStageBoosterModifier; 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 // Forced X_ATTACK to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => { game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
@ -158,16 +153,17 @@ describe("Items - Temporary Stat Stage Boosters", () => {
handler.setCursor(0); handler.setCursor(0);
handler.setRowCursor(ShopCursorTarget.REWARDS); handler.setRowCursor(ShopCursorTarget.REWARDS);
handler.processInput(Button.ACTION); 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 // Making sure only one booster is in the modifier list even after picking up another
let count = 0; let count = 0;
for (const m of game.scene.modifiers) { for (const m of game.scene.modifiers) {
if (m instanceof TempStatStageBoosterModifier) { if (m instanceof TempStatStageBoosterModifier) {
count++; count++;
expect((m as TempStatStageBoosterModifier).getBattlesLeft()).toBe(5); const modifierInstance = m as TempStatStageBoosterModifier;
expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles());
} }
} }
expect(count).toBe(1); expect(count).toBe(1);

View File

@ -455,6 +455,26 @@ export function rgbaToInt(rgba: integer[]): integer {
return (rgba[0] << 24) + (rgba[1] << 16) + (rgba[2] << 8) + rgba[3]; 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 /*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 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: This function is used in: