Skip to content

Commit

Permalink
Merge pull request #1110 from wowsims/feature/reforge-breakpoint-limits
Browse files Browse the repository at this point in the history
Add ability to limit breakpoint capping
  • Loading branch information
1337LutZ authored Oct 21, 2024
2 parents fe9ace2 + f45fe8b commit 74e1ef2
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 24 deletions.
1 change: 1 addition & 0 deletions proto/ui.proto
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ message IndividualSimSettings {
Stat heal_ref_stat = 13;
Stat tank_ref_stat = 14;
UnitStats stat_caps = 15;
UnitStats breakpoint_limits = 16;
}

message StatCapConfig {
Expand Down
158 changes: 136 additions & 22 deletions ui/core/components/suggest_reforges_action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type ReforgeOptimizerOptions = {
experimental?: true;
statTooltips?: StatTooltipContent;
statSelectionPresets?: UnitStatPresets[];
// Allows you to enable breakpoint limits for Treshold type caps
enableBreakpointLimits?: boolean;
// Allows you to modify the stats before they are returned for the calculations
// For example: Adding class specific Glyphs/Talents that are not added by the backend
updateGearStatsModifier?: (baseStats: Stats) => Stats;
Expand All @@ -83,6 +85,7 @@ export class ReforgeOptimizer {
protected updateGearStatsModifier: ReforgeOptimizerOptions['updateGearStatsModifier'];
protected _softCapsConfig: StatCap[];
protected updateSoftCaps: ReforgeOptimizerOptions['updateSoftCaps'];
protected enableBreakpointLimits: ReforgeOptimizerOptions['enableBreakpointLimits'];
protected statTooltips: StatTooltipContent = {};
protected additionalSoftCapTooltipInformation: StatTooltipContent = {};
protected statSelectionPresets: ReforgeOptimizerOptions['statSelectionPresets'];
Expand All @@ -108,6 +111,7 @@ export class ReforgeOptimizer {
this.additionalSoftCapTooltipInformation = { ...options?.additionalSoftCapTooltipInformation };
this.statSelectionPresets = options?.statSelectionPresets;
this._statCaps = this.statCaps;
this.enableBreakpointLimits = !!options?.enableBreakpointLimits;

const startReforgeOptimizationEntry: ActionGroupItem = {
label: 'Suggest Reforges',
Expand Down Expand Up @@ -188,6 +192,18 @@ export class ReforgeOptimizer {
return this.updateSoftCaps?.(StatCap.cloneSoftCaps(this._softCapsConfig)) || this._softCapsConfig;
}

get softCapsConfigWithLimits() {
if (!this.enableBreakpointLimits) return this.softCapsConfig;

const softCaps = StatCap.cloneSoftCaps(this.softCapsConfig);
for (const [unitStat, limit] of this.player.getBreakpointLimits().asUnitStatArray()) {
if (!limit) continue;
const config = softCaps.find(config => config.unitStat.equals(unitStat));
if (config) config.breakpoints = config.breakpoints.filter(breakpoint => breakpoint <= limit);
}
return softCaps;
}

get statCaps() {
return this.sim.getUseCustomEPValues() ? this.player.getStatCaps() : this.defaults.statCaps || new Stats();
}
Expand Down Expand Up @@ -254,11 +270,11 @@ export class ReforgeOptimizer {
<p>The following breakpoints have been implemented for this spec:</p>
<table className="w-100">
<tbody>
{this.softCapsConfig?.map(({ unitStat, breakpoints, capType, postCapEPs }, index) => (
{this.softCapsConfigWithLimits?.map(({ unitStat, breakpoints, capType, postCapEPs }, index) => (
<>
<tr>
<th className="text-nowrap" colSpan={2}>
{unitStat.getShortName(this.player.getClass())}
{unitStat.getShortName(this.playerClass)}
</th>
<td className="text-end">{statCapTypeNames.get(capType)}</td>
</tr>
Expand All @@ -282,22 +298,15 @@ export class ReforgeOptimizer {
</tr>
{breakpoints.map((breakpoint, breakpointIndex) => (
<tr>
<td className="text-end">
{unitStat.equalsStat(Stat.StatMasteryRating)
? (
(breakpoint / Mechanics.MASTERY_RATING_PER_MASTERY_POINT) *
this.player.getMasteryPerPointModifier()
).toFixed(2)
: unitStat.convertDefaultUnitsToPercent(breakpoint)!.toFixed(2)}
</td>
<td className="text-end">{this.breakpointValueToDisplayPercentage(breakpoint, unitStat)}</td>
<td colSpan={2} className="text-end">
{unitStat
.convertEpToRatingScale(capType === StatCapType.TypeThreshold ? postCapEPs[0] : postCapEPs[breakpointIndex])
.toFixed(2)}
</td>
</tr>
))}
{index !== this.softCapsConfig.length - 1 && (
{index !== this.softCapsConfigWithLimits.length - 1 && (
<>
<tr>
<td colSpan={3} className="border-bottom pb-2"></td>
Expand Down Expand Up @@ -381,6 +390,7 @@ export class ReforgeOptimizer {
description: descriptionRef.value!,
})}
{useSoftCapBreakpointsInput?.rootElem}
{this.buildSoftCapBreakpointsLimiter({ useSoftCapBreakpointsInput })}
{freezeItemSlotsInput.rootElem}
{this.buildFrozenSlotsInputs()}
{this.buildEPWeightsToggle({ useCustomEPValuesInput: useCustomEPValuesInput })}
Expand Down Expand Up @@ -476,17 +486,10 @@ export class ReforgeOptimizer {

const sharedStatInputConfig: Pick<NumberPickerConfig<Player<any>>, 'getValue' | 'setValue'> = {
getValue: () => {
const rawStatValue = this.statCaps.getUnitStat(unitStat);
let percentOrPointsValue = unitStat.convertDefaultUnitsToPercent(rawStatValue)!;
if (unitStat.equalsStat(Stat.StatMasteryRating))
percentOrPointsValue = this.toVisualTotalMasteryPercentage(percentOrPointsValue, rawStatValue);

return percentOrPointsValue;
return this.toVisualUnitStatPercentage(this.statCaps.getUnitStat(unitStat), unitStat);
},
setValue: (_eventID, _player, newValue) => {
let statValue = unitStat.convertPercentToDefaultUnits(newValue)!;
if (unitStat.equalsStat(Stat.StatMasteryRating)) statValue /= this.player.getMasteryPerPointModifier();
this.setStatCap(unitStat, statValue);
this.setStatCap(unitStat, this.toDefaultUnitStatValue(newValue, unitStat));
},
};

Expand Down Expand Up @@ -625,14 +628,105 @@ export class ReforgeOptimizer {
);
}

buildSoftCapBreakpointsLimiter({ useSoftCapBreakpointsInput }: { useSoftCapBreakpointsInput: BooleanPicker<Player<any>> | null }) {
if (!this.enableBreakpointLimits || !useSoftCapBreakpointsInput) return null;

const tableRef = ref<HTMLTableElement>();
const breakpointsLimitTooltipRef = ref<HTMLButtonElement>();

const content = (
<table ref={tableRef} className={clsx('reforge-optimizer-stat-cap-table mb-2', !this.sim.getUseSoftCapBreakpoints() && 'hide')}>
<thead>
<tr>
<th colSpan={3} className="pb-3">
<div className="d-flex">
<h6 className="content-block-title mb-0 me-1">Breakpoint limit</h6>
<button ref={breakpointsLimitTooltipRef} className="d-inline">
<i className="fa-regular fa-circle-question" />
</button>
</div>
</th>
</tr>
</thead>
<tbody>
{this.softCapsConfig
.filter(config => config.capType === StatCapType.TypeThreshold && config.breakpoints.length > 1)
.map(({ breakpoints, unitStat }) => {
if (!unitStat.hasRootStat()) return;
const rootStat = unitStat.getRootStat();
if (!INCLUDED_STATS.includes(rootStat)) return;

const listElementRef = ref<HTMLTableRowElement>();
const statName = unitStat.getShortName(this.player.getClass());
const picker = !!breakpoints
? new EnumPicker(null, this.player, {
id: `reforge-optimizer-${statName}-presets`,
extraCssClasses: ['mb-0'],
label: '',
values: [
{ name: 'No limit set', value: 0 },
...breakpoints.map(breakpoint => ({
name: `${this.breakpointValueToDisplayPercentage(breakpoint, unitStat)}%`,
value: breakpoint,
})),
].sort((a, b) => a.value - b.value),
changedEvent: _ => TypedEvent.onAny([this.sim.useSoftCapBreakpointsChangeEmitter]),
getValue: () => {
return this.player.getBreakpointLimits().getUnitStat(unitStat) || 0;
},
setValue: (eventID, _player, newValue) => {
this.player.setBreakpointLimits(eventID, this.player.getBreakpointLimits().withUnitStat(unitStat, newValue));
},
})
: null;

if (!picker?.rootElem) return null;

const row = (
<>
<tr ref={listElementRef} className="reforge-optimizer-stat-cap-item">
<td>
<div className="reforge-optimizer-stat-cap-item-label">{statName}</div>
</td>
<td colSpan={2}>{picker.rootElem}</td>
</tr>
</>
);

return row;
})}
</tbody>
</table>
);

if (breakpointsLimitTooltipRef.value) {
const tooltip = tippy(breakpointsLimitTooltipRef.value, {
content: 'Allows you to set a custom breakpoint limit.',
});
useSoftCapBreakpointsInput.addOnDisposeCallback(() => tooltip.destroy());
}

const event = this.sim.useSoftCapBreakpointsChangeEmitter.on(() => {
const isUsingBreakpoints = this.sim.getUseSoftCapBreakpoints();
tableRef.value?.classList[isUsingBreakpoints ? 'remove' : 'add']('hide');
});

useSoftCapBreakpointsInput.addOnDisposeCallback(() => {
content.remove();
event?.dispose();
});

return content;
}

get isAllowedToOverrideStatCaps() {
return !(this.sim.getUseSoftCapBreakpoints() && this.softCapsConfig);
}

get processedStatCaps() {
let statCaps = this.statCaps;
if (!this.isAllowedToOverrideStatCaps)
this.softCapsConfig.forEach(({ unitStat }) => {
this.softCapsConfigWithLimits.forEach(({ unitStat }) => {
statCaps = statCaps.withUnitStat(unitStat, 0);
});

Expand Down Expand Up @@ -689,7 +783,7 @@ export class ReforgeOptimizer {
const reforgeSoftCaps: StatCap[] = [];

if (!this.isAllowedToOverrideStatCaps) {
this.softCapsConfig.slice().forEach(config => {
this.softCapsConfigWithLimits.slice().forEach(config => {
let weights = config.postCapEPs.slice();
const relativeBreakpoints = [];

Expand Down Expand Up @@ -1003,6 +1097,26 @@ export class ReforgeOptimizer {
return statPoints;
}

private toVisualUnitStatPercentage(statValue: number, unitStat: UnitStat) {
const rawStatValue = statValue;
let percentOrPointsValue = unitStat.convertDefaultUnitsToPercent(rawStatValue)!;
if (unitStat.equalsStat(Stat.StatMasteryRating)) percentOrPointsValue = this.toVisualTotalMasteryPercentage(percentOrPointsValue, rawStatValue);

return percentOrPointsValue;
}

private toDefaultUnitStatValue(value: number, unitStat: UnitStat) {
let statValue = unitStat.convertPercentToDefaultUnits(value)!;
if (unitStat.equalsStat(Stat.StatMasteryRating)) statValue /= this.player.getMasteryPerPointModifier();
return statValue;
}

private breakpointValueToDisplayPercentage(value: number, unitStat: UnitStat) {
return unitStat.equalsStat(Stat.StatMasteryRating)
? ((value / Mechanics.MASTERY_RATING_PER_MASTERY_POINT) * this.player.getMasteryPerPointModifier()).toFixed(2)
: unitStat.convertDefaultUnitsToPercent(value)!.toFixed(2);
}

onReforgeDone() {
const itemSlots = this.player.getGear().getItemSlots();
const changedSlots = new Map<ItemSlot, ReforgeData | undefined>();
Expand Down
6 changes: 6 additions & 0 deletions ui/core/individual_sim_ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
if (this.individualConfig.defaults.statCaps) this.player.setStatCaps(eventID, this.individualConfig.defaults.statCaps);
if (this.individualConfig.defaults.softCapBreakpoints)
this.player.setSoftCapBreakpoints(eventID, this.individualConfig.defaults.softCapBreakpoints);
this.player.setBreakpointLimits(eventID, new Stats());
this.player.setProfession1(eventID, this.individualConfig.defaults.other?.profession1 || Profession.Engineering);
this.player.setProfession2(eventID, this.individualConfig.defaults.other?.profession2 || Profession.Jewelcrafting);
this.player.setDistanceFromTarget(eventID, this.individualConfig.defaults.other?.distanceFromTarget || 0);
Expand Down Expand Up @@ -618,6 +619,7 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
epWeightsStats: this.player.getEpWeights().toProto(),
epRatios: this.player.getEpRatios(),
statCaps: this.player.getStatCaps().toProto(),
breakpointLimits: this.player.getBreakpointLimits().toProto(),
dpsRefStat: this.dpsRefStat,
healRefStat: this.healRefStat,
tankRefStat: this.tankRefStat,
Expand Down Expand Up @@ -678,6 +680,10 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
this.player.setStatCaps(eventID, Stats.fromProto(settings.statCaps));
}

if (settings.breakpointLimits) {
this.player.setBreakpointLimits(eventID, Stats.fromProto(settings.breakpointLimits));
}

if (settings.dpsRefStat) {
this.dpsRefStat = settings.dpsRefStat;
}
Expand Down
11 changes: 11 additions & 0 deletions ui/core/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export class Player<SpecType extends Spec> {
private epWeights: Stats = new Stats();
private statCaps: Stats = new Stats();
private softCapBreakpoints: StatCap[] = [];
private breakpointLimits: Stats = new Stats();
private currentStats: PlayerStats = PlayerStats.create();
private metadata: UnitMetadata = new UnitMetadata();
private petMetadatas: UnitMetadataList = new UnitMetadataList();
Expand All @@ -292,6 +293,7 @@ export class Player<SpecType extends Spec> {
readonly epWeightsChangeEmitter = new TypedEvent<void>('PlayerEpWeights');
readonly statCapsChangeEmitter = new TypedEvent<void>('StatCaps');
readonly softCapBreakpointsChangeEmitter = new TypedEvent<void>('SoftCapBreakpoints');
readonly breakpointLimitsChangeEmitter = new TypedEvent<void>('BreakpointLimits');
readonly miscOptionsChangeEmitter = new TypedEvent<void>('PlayerMiscOptions');

readonly currentStatsEmitter = new TypedEvent<void>('PlayerCurrentStats');
Expand Down Expand Up @@ -351,6 +353,7 @@ export class Player<SpecType extends Spec> {
this.epRatiosChangeEmitter,
this.epRefStatChangeEmitter,
this.statCapsChangeEmitter,
this.breakpointLimitsChangeEmitter,
],
'PlayerChange',
);
Expand Down Expand Up @@ -499,6 +502,14 @@ export class Player<SpecType extends Spec> {
this.softCapBreakpoints = newSoftCapBreakpoints;
this.softCapBreakpointsChangeEmitter.emit(eventID);
}
getBreakpointLimits(): Stats {
return this.breakpointLimits;
}

setBreakpointLimits(eventID: EventID, newLimits: Stats) {
this.breakpointLimits = newLimits;
this.breakpointLimitsChangeEmitter.emit(eventID);
}

getDefaultEpRatios(isTankSpec: boolean, isHealingSpec: boolean): Array<number> {
const defaultRatios = new Array(Player.numEpRatios).fill(0);
Expand Down
8 changes: 8 additions & 0 deletions ui/core/proto_utils/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ export class UnitStat {
}
}

toJson(): object {
return UnitStatProto.toJson(this.toProto()) as object;
}

static fromJson(obj: any): UnitStat {
return UnitStat.fromProto(UnitStatProto.fromJson(obj));
}

toProto(): UnitStatProto {
const protoMessage = UnitStatProto.create({});

Expand Down
5 changes: 3 additions & 2 deletions ui/mage/fire/sim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export class FireMageSimUI extends IndividualSimUI<Spec.SpecFireMage> {
player.sim.waitForInit().then(() => {
new ReforgeOptimizer(this, {
statSelectionPresets: Presets.FIRE_BREAKPOINTS,
enableBreakpointLimits: true,
updateSoftCaps: softCaps => {
const raidBuffs = player.getRaid()?.getBuffs();
const hasBL = !!(raidBuffs?.bloodlust || raidBuffs?.timeWarp || raidBuffs?.heroism);
Expand Down Expand Up @@ -271,7 +272,7 @@ export class FireMageSimUI extends IndividualSimUI<Spec.SpecFireMage> {
if (!hasCloseMatchingValue(blBreakpoint)) adjustedHastedBreakpoints.add(blBreakpoint);
if (hasBerserking) {
const berserkingBreakpoint = modifyHaste(blBreakpoint, 1.2);
if (berserkingBreakpoint > 0 && !hasCloseMatchingValue(blBreakpoint)) {
if (berserkingBreakpoint > 0 && !hasCloseMatchingValue(berserkingBreakpoint)) {
adjustedHastedBreakpoints.add(berserkingBreakpoint);
}
}
Expand All @@ -283,7 +284,7 @@ export class FireMageSimUI extends IndividualSimUI<Spec.SpecFireMage> {
if (!hasCloseMatchingValue(piBreakpoint)) adjustedHastedBreakpoints.add(piBreakpoint);
if (hasBerserking) {
const berserkingBreakpoint = modifyHaste(piBreakpoint, 1.2);
if (berserkingBreakpoint > 0 && !hasCloseMatchingValue(piBreakpoint)) {
if (berserkingBreakpoint > 0 && !hasCloseMatchingValue(berserkingBreakpoint)) {
adjustedHastedBreakpoints.add(berserkingBreakpoint);
}
}
Expand Down

0 comments on commit 74e1ef2

Please sign in to comment.