Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions proto/ui.proto
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ message SavedGearSet {
UnitStats bonus_stats_stats = 3;
}

message SavedStatWeightSettings {
UnitStats excluded_from_calc = 1;
}

// Local storage data for other settings.
message SavedSettings {
RaidBuffs raid_buffs = 1;
Expand Down
1 change: 1 addition & 0 deletions ui/core/components/detailed_results/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export class Timeline extends ResultComponent {
const { dpsResourcesPlotOptions, rotationLabels, rotationTimeline, rotationHiddenIdsContainer, rotationTimelineTimeRulerImage } = cachedData;
this.rotationLabels.replaceChildren(...rotationLabels.cloneNode(true).childNodes);
this.rotationTimeline.replaceChildren(...rotationTimeline.cloneNode(true).childNodes);

this.rotationHiddenIdsContainer.replaceChildren(...rotationHiddenIdsContainer.cloneNode(true).childNodes);
this.dpsResourcesPlot.updateOptions(dpsResourcesPlotOptions);

Expand Down
160 changes: 149 additions & 11 deletions ui/core/components/stat_weights_action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,106 @@ import { ref } from 'tsx-vanilla';

import { IndividualSimUI } from '../individual_sim_ui.jsx';
import { Player } from '../player.js';
import { ProgressMetrics, StatWeightsResult, StatWeightValues } from '../proto/api.js';
import { ProgressMetrics, StatWeightsResult, StatWeightValues } from '../proto/api';
import { PseudoStat, Stat, UnitStats } from '../proto/common.js';
import { SavedStatWeightSettings } from '../proto/ui';
import { getStatName } from '../proto_utils/names.js';
import { Stats, UnitStat } from '../proto_utils/stats.js';
import { RequestTypes } from '../sim_signal_manager';
import { SimUI } from '../sim_ui';
import { EventID, TypedEvent } from '../typed_event.js';
import { stDevToConf90 } from '../utils.js';
import { BaseModal } from './base_modal.jsx';
import { BooleanPicker } from './pickers/boolean_picker.js';
import { NumberPicker } from './pickers/number_picker.js';
import { ResultsViewer } from './results_viewer.jsx';
import { renderSavedEPWeights } from './saved_data_managers/ep_weights';
export class StatWeightActionSettings {
private readonly storageKey: string;
readonly changeEmitter = new TypedEvent<void>();

export const addStatWeightsAction = (simUI: IndividualSimUI<any>) => {
const epWeightsModal = new EpWeightsMenu(simUI);
excludedFromCalc = UnitStats.create();

constructor(simUI: SimUI) {
this.storageKey = simUI.getStorageKey('__statweight_settings__');
this.changeEmitter.on(() => {
const json = SavedStatWeightSettings.toJsonString(this.toProto());
window.localStorage.setItem(this.storageKey, json);
});
}

applyDefaults(eventID: EventID) {
this.excludedFromCalc = UnitStats.create();
this.changeEmitter.emit(eventID);
}

load(eventID: EventID) {
const storageValue = window.localStorage.getItem(this.storageKey);
if (storageValue) {
const loaded = SavedStatWeightSettings.fromJsonString(storageValue);
if (loaded.excludedFromCalc) this.excludedFromCalc = loaded.excludedFromCalc;
this.changeEmitter.emit(eventID);
}
}

toProto(): SavedStatWeightSettings {
const proto = SavedStatWeightSettings.create();
proto.excludedFromCalc = this.excludedFromCalc;
return proto;
}

/**
* Check if a stat should be excluded from weight calculation.
* @param stat
* @returns true if stat should be excluded.
*/
isStatExcludedFromCalc(stat: Stat): boolean {
return !!this.excludedFromCalc?.stats.includes(stat);
}

/**
* Check if a pseudostat should be excluded from weight calculation.
* @param pseudoStat
* @returns true if pseudostat should be excluded.
*/
isPseudostatExcludedFromCalc(pseudoStat: PseudoStat): boolean {
return !!this.excludedFromCalc?.pseudoStats.includes(pseudoStat);
}

/**
* Check if a unitstat should be excluded from weight calculation.
* @param unitstat
* @returns true if unitstat should be excluded.
*/
isUnitStatExcludedFromCalc(unitstat: UnitStat): boolean {
return unitstat.isStat() ? this.isStatExcludedFromCalc(unitstat.getStat()) : this.isPseudostatExcludedFromCalc(unitstat.getPseudoStat());
}

/**
* Set whether a stat should be excluded from calculation.
* @param stat
* @param exclude
*/
setStatExcluded(eventID: EventID, stat: UnitStat, exclude: boolean) {
const updateStatEntry = <T extends Stat | PseudoStat>(s: T, target: T[]) => {
const currentIdx = target.indexOf(s);
if (exclude) {
if (currentIdx === -1) target.push(s);
} else if (currentIdx !== -1) {
target.splice(currentIdx, 1);
}
};
if (stat.isStat()) {
updateStatEntry(stat.getStat(), this.excludedFromCalc.stats);
} else {
updateStatEntry(stat.getPseudoStat(), this.excludedFromCalc.pseudoStats);
}
this.changeEmitter.emit(eventID);
}
}

export const addStatWeightsAction = (simUI: IndividualSimUI<any>, settings: StatWeightActionSettings) => {
const epWeightsModal = new EpWeightsMenu(simUI, settings);
simUI.addAction('Stat Weights', 'ep-weights-action', () => {
epWeightsModal.open();
});
Expand Down Expand Up @@ -55,14 +140,15 @@ export class EpWeightsMenu extends BaseModal {
private readonly table: HTMLElement;
private readonly tableBody: HTMLElement;
private readonly resultsViewer: ResultsViewer;
private readonly settings: StatWeightActionSettings;

private statsType: string;
private epStats: Stat[];
private epPseudoStats: PseudoStat[];
private epReferenceStat: Stat;
private showAllStats = false;

constructor(simUI: IndividualSimUI<any>) {
constructor(simUI: IndividualSimUI<any>, settings: StatWeightActionSettings) {
super(simUI.rootElem, 'ep-weights-menu', { ...getModalConfig(simUI), disposeOnClose: false });
this.header?.insertAdjacentElement('afterbegin', <h5 className="modal-title">Calculate Stat Weights</h5>);

Expand All @@ -71,6 +157,7 @@ export class EpWeightsMenu extends BaseModal {
this.epStats = this.simUI.individualConfig.epStats;
this.epPseudoStats = this.simUI.individualConfig.epPseudoStats || [];
this.epReferenceStat = this.simUI.individualConfig.epReferenceStat;
this.settings = settings;

const statsTable = this.buildStatsTable();
const containerRef = ref<HTMLDivElement>();
Expand Down Expand Up @@ -138,6 +225,7 @@ export class EpWeightsMenu extends BaseModal {
<thead>
<tr>
<th>Stat</th>
<th>Update</th>
{statsTable.map(({ metric, type, label, metricRef }) => {
const isAction = type === 'action';
return (
Expand All @@ -152,6 +240,7 @@ export class EpWeightsMenu extends BaseModal {
</tr>
<tr className="ep-ratios">
<td>EP Ratio</td>
<td></td>
{statsTable
.filter(({ type }) => type !== 'action')
.map(({ metric, type, ratioRef }) => (
Expand Down Expand Up @@ -277,10 +366,13 @@ export class EpWeightsMenu extends BaseModal {
}
});

const epStatsToCalc = this.epStats.filter(s => !this.settings.isStatExcludedFromCalc(s));
const epPseudoStatsToCalc = this.epPseudoStats.filter(ps => !this.settings.isPseudostatExcludedFromCalc(ps));

const result = await this.simUI.player.computeStatWeights(
TypedEvent.nextEventID(),
this.epStats,
this.epPseudoStats,
epStatsToCalc,
epPseudoStatsToCalc,
this.epReferenceStat,
progress => {
this.setSimProgress(progress);
Expand Down Expand Up @@ -326,7 +418,7 @@ export class EpWeightsMenu extends BaseModal {
});

button.addEventListener('click', () => {
this.simUI.player.setEpWeights(TypedEvent.nextEventID(), Stats.fromProto(weightsFunc()));
this.setEpWeightsWithoutExcluded(Stats.fromProto(weightsFunc()));
this.updateTable();
});
};
Expand Down Expand Up @@ -384,7 +476,7 @@ export class EpWeightsMenu extends BaseModal {
const scaledTmiEp = Stats.fromProto(results.tmi!.epValues).scale(epRatios[4]);
const scaledPDeathEp = Stats.fromProto(results.pDeath!.epValues).scale(epRatios[5]);
const newEp = scaledDpsEp.add(scaledHpsEp).add(scaledTpsEp).add(scaledDtpsEp).add(scaledTmiEp).add(scaledPDeathEp);
this.simUI.player.setEpWeights(TypedEvent.nextEventID(), newEp);
this.setEpWeightsWithoutExcluded(newEp);
} else {
const scaledDpsWeights = Stats.fromProto(results.dps!.weights).scale(epRatios[0]);
const scaledHpsWeights = Stats.fromProto(results.hps!.weights).scale(epRatios[1]);
Expand All @@ -398,14 +490,40 @@ export class EpWeightsMenu extends BaseModal {
.add(scaledDtpsWeights)
.add(scaledTmiWeights)
.add(scaledPDeathWeights);
this.simUI.player.setEpWeights(TypedEvent.nextEventID(), newWeights);
this.setEpWeightsWithoutExcluded(newWeights);
}
this.updateTable();
});

this.buildSavedEPWeightsPicker();
}

/**
* Set new ep weights while leaving excluded stats at their old value.
* @param newWeights
*/
private setEpWeightsWithoutExcluded(newWeights: Stats) {
const excludedStats = this.settings.excludedFromCalc;
const oldWeights = this.simUI.player.getEpWeights();
for (const stat of excludedStats.stats) {
newWeights = newWeights.withStat(stat, oldWeights.getStat(stat));
}
for (const pseudoStat of excludedStats.pseudoStats) {
newWeights = newWeights.withPseudoStat(pseudoStat, oldWeights.getPseudoStat(pseudoStat));
}
this.simUI.player.setEpWeights(TypedEvent.nextEventID(), newWeights);
}

/**
* Check if a specific stat is included in the EP stats for this spec.
* @param stat
* @returns
*/
private isEpStat(stat: UnitStat) {
if (stat.isStat()) return this.epStats.includes(stat.getStat());
return this.epPseudoStats.includes(stat.getPseudoStat());
}

private setSimProgress(progress: ProgressMetrics) {
this.resultsViewer.setContent(
<div className="results-sim">
Expand Down Expand Up @@ -440,13 +558,15 @@ export class EpWeightsMenu extends BaseModal {
}

private makeTableRow(stat: UnitStat): HTMLElement {
const result = this.simUI.prevEpSimResult;
const result = !this.settings.isUnitStatExcludedFromCalc(stat) ? this.simUI.prevEpSimResult : null;
const epRatios = this.simUI.player.getEpRatios();
const rowTotalEp = scaledEpValue(stat, epRatios, result);
const currentEpRef = ref<HTMLTableCellElement>();
const includeToggleRef = ref<HTMLTableCellElement>();
const row = (
<tr>
<td>{stat.getFullName(this.simUI.player.getClass())}</td>
<td ref={includeToggleRef} className="swcalc-include-toggle"></td>
{this.makeTableRowCells(stat, result?.dps, 'damage-metrics', rowTotalEp, epRatios[0])}
{this.makeTableRowCells(stat, result?.hps, 'healing-metrics', rowTotalEp, epRatios[1])}
{this.makeTableRowCells(stat, result?.tps, 'threat-metrics', rowTotalEp, epRatios[2])}
Expand All @@ -457,6 +577,16 @@ export class EpWeightsMenu extends BaseModal {
</tr>
) as HTMLElement;

if (includeToggleRef.value && this.isEpStat(stat)) {
new BooleanPicker(includeToggleRef.value, this, {
id: 'sw-stat-toggle-' + stat.getFullName(this.simUI.player.getClass()),
getValue: epWeightsModal => !epWeightsModal.settings.isUnitStatExcludedFromCalc(stat),
setValue: (eventID, epWeightsModal, newValue) => epWeightsModal.settings.setStatExcluded(eventID, stat, !newValue),
changedEvent: epWeightsModal => epWeightsModal.settings.changeEmitter,
enableWhen: epWeightsModal => !stat.isStat() || epWeightsModal.epReferenceStat != stat.getStat(),
});
}

const currentEpCell = currentEpRef.value!;
new NumberPicker(currentEpCell, this.simUI.player, {
id: `ep-weight-stat-${stat}`,
Expand Down Expand Up @@ -620,7 +750,15 @@ export class EpWeightsMenu extends BaseModal {
if (stat.isStat()) {
return true;
} else {
return [PseudoStat.PseudoStatMainHandDps, PseudoStat.PseudoStatOffHandDps, PseudoStat.PseudoStatRangedDps, PseudoStat.PseudoStatPhysicalHitPercent, PseudoStat.PseudoStatSpellHitPercent, PseudoStat.PseudoStatPhysicalCritPercent, PseudoStat.PseudoStatSpellCritPercent].includes(stat.getPseudoStat());
return [
PseudoStat.PseudoStatMainHandDps,
PseudoStat.PseudoStatOffHandDps,
PseudoStat.PseudoStatRangedDps,
PseudoStat.PseudoStatPhysicalHitPercent,
PseudoStat.PseudoStatSpellHitPercent,
PseudoStat.PseudoStatPhysicalCritPercent,
PseudoStat.PseudoStatSpellCritPercent,
].includes(stat.getPseudoStat());
}
});

Expand Down
8 changes: 6 additions & 2 deletions ui/core/individual_sim_ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import * as InputHelpers from './components/input_helpers';
import { ItemNotice } from './components/item_notice/item_notice';
import { addRaidSimAction, RaidSimResultsManager } from './components/raid_sim_action';
import { SavedDataConfig } from './components/saved_data_manager';
import { addStatWeightsAction, EpWeightsMenu } from './components/stat_weights_action';
import { addStatWeightsAction, EpWeightsMenu, StatWeightActionSettings } from './components/stat_weights_action';
import { SimSettingCategories } from './constants/sim_settings';
import * as Tooltips from './constants/tooltips';
import { getSpecLaunchStatus, LaunchStatus, simLaunchStatuses } from './launched_sims';
Expand Down Expand Up @@ -211,6 +211,7 @@ export interface Settings {
export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
readonly player: Player<SpecType>;
readonly individualConfig: IndividualSimUIConfig<SpecType>;
private readonly statWeightActionSettings: StatWeightActionSettings;

private raidSimResultsManager: RaidSimResultsManager | null;
epWeightsModal: EpWeightsMenu | null = null;
Expand All @@ -237,6 +238,7 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
this.raidSimResultsManager = null;
this.prevEpIterations = 0;
this.prevEpSimResult = null;
this.statWeightActionSettings = new StatWeightActionSettings(this);

if (!isDevMode() && getSpecLaunchStatus(this.player) === LaunchStatus.Unlaunched) {
this.handleSimUnlaunched();
Expand Down Expand Up @@ -400,7 +402,7 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
private addSidebarComponents() {
this.raidSimResultsManager = addRaidSimAction(this);
this.sim.waitForInit().then(() => {
this.epWeightsModal = addStatWeightsAction(this);
this.epWeightsModal = addStatWeightsAction(this, this.statWeightActionSettings);
});

new CharacterStats(
Expand Down Expand Up @@ -577,6 +579,8 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
} else {
this.sim.raid.setTanks(eventID, []);
}

this.statWeightActionSettings.applyDefaults(eventID);
}
});
}
Expand Down
Loading