[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.
|
* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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%."
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 { 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);
|
||||||
|
|
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];
|
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:
|
||||||
|
|
Loading…
Reference in New Issue