From e845e088dcbf86fc2a9592c6bd2fdc18bd424b38 Mon Sep 17 00:00:00 2001 From: Kayla Glick <12898988+kayla-glick@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:26:21 -0500 Subject: [PATCH] revamp logs tab UI (#4224) --- .vscode/settings.json | 18 +- sim/core/pet.go | 6 +- sim/core/sim.go | 5 - ui/core/components/boolean_picker.ts | 14 +- .../components/detailed_results/log_runner.ts | 31 ---- .../detailed_results/log_runner.tsx | 79 +++++++++ .../components/detailed_results/timeline.tsx | 159 ++++++++++-------- .../individual_sim_ui/rotation_tab.ts | 25 ++- .../individual_sim_ui/settings_tab.ts | 17 +- .../{logs_parser.ts => logs_parser.tsx} | 147 +++++++++++----- ui/core/utils.ts | 9 +- .../core/components/_detailed_results.scss | 13 -- ui/scss/core/components/_icon_picker.scss | 17 +- .../detailed_results/_log_runner.scss | 61 ++++++- .../detailed_results/_timeline.scss | 51 +----- ui/scss/shared/_global.scss | 28 ++- ui/scss/shared/_variables.scss | 32 +++- 17 files changed, 431 insertions(+), 281 deletions(-) delete mode 100644 ui/core/components/detailed_results/log_runner.ts create mode 100644 ui/core/components/detailed_results/log_runner.tsx rename ui/core/proto_utils/{logs_parser.ts => logs_parser.tsx} (82%) diff --git a/.vscode/settings.json b/.vscode/settings.json index c9d88ad7ab..8924ec6f05 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,16 +2,22 @@ "eslint.validate": ["javascript", "typescript"], "eslint.nodePath": "./node_modules", "eslint.workingDirectories": ["."], - "files.associations": { - "*.js": "javascript", - "*.ts": "typescript" + "[javascript]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } + }, + "[javascriptreact]": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } }, "[typescript]": { "editor.codeActionsOnSave": { "source.fixAll.eslint": "always" } }, - "[javascript]": { + "[typescriptreact]": { "editor.codeActionsOnSave": { "source.fixAll.eslint": "always" } @@ -26,10 +32,6 @@ "source.fixAll.eslint": "always" } }, - "[json]": { - "editor.formatOnSave": true, - }, - "json.format.enable": true, "json.schemas": [ { "fileMatch": [ diff --git a/sim/core/pet.go b/sim/core/pet.go index a06684fbec..b403118647 100644 --- a/sim/core/pet.go +++ b/sim/core/pet.go @@ -164,8 +164,8 @@ func (pet *Pet) Enable(sim *Simulation, petAgent PetAgent) { } if sim.Log != nil { - pet.Log(sim, "Pet stats: %s", pet.GetStats()) - pet.Log(sim, "Pet inherited stats: %s", pet.ApplyStatDependencies(pet.inheritedStats)) + pet.Log(sim, "Pet stats: %s", pet.GetStats().FlatString()) + pet.Log(sim, "Pet inherited stats: %s", pet.ApplyStatDependencies(pet.inheritedStats).FlatString()) pet.Log(sim, "Pet summoned") } @@ -256,7 +256,7 @@ func (pet *Pet) Disable(sim *Simulation) { if sim.Log != nil { pet.Log(sim, "Pet dismissed") - pet.Log(sim, pet.GetStats().String()) + pet.Log(sim, pet.GetStats().FlatString()) } } diff --git a/sim/core/sim.go b/sim/core/sim.go index 6d22f6c87a..735d7a3aaa 100644 --- a/sim/core/sim.go +++ b/sim/core/sim.go @@ -364,11 +364,6 @@ var ( // Reset will set sim back and erase all current state. // This is automatically called before every 'Run'. func (sim *Simulation) reset() { - if sim.Log != nil { - sim.Log("SIM RESET") - sim.Log("----------------------") - } - if sim.Encounter.DurationIsEstimate && sim.CurrentTime != 0 { sim.BaseDuration = sim.CurrentTime sim.Encounter.DurationIsEstimate = false diff --git a/ui/core/components/boolean_picker.ts b/ui/core/components/boolean_picker.ts index f83e806f87..2b2c861d6c 100644 --- a/ui/core/components/boolean_picker.ts +++ b/ui/core/components/boolean_picker.ts @@ -1,12 +1,11 @@ -import { EventID, TypedEvent } from '../typed_event.js'; - +import { TypedEvent } from '../typed_event.js'; import { Input, InputConfig } from './input.js'; /** * Data for creating a boolean picker (checkbox). */ export interface BooleanPickerConfig extends InputConfig { - cssScheme?: string; + reverse?: boolean; } // UI element for picking an arbitrary number field. @@ -21,11 +20,16 @@ export class BooleanPicker extends Input { this.inputElem = document.createElement('input'); this.inputElem.type = 'checkbox'; this.inputElem.classList.add('boolean-picker-input', 'form-check-input'); - this.rootElem.appendChild(this.inputElem); + + if (config.reverse) { + this.rootElem.prepend(this.inputElem); + } else { + this.rootElem.appendChild(this.inputElem); + } this.init(); - this.inputElem.addEventListener('change', event => { + this.inputElem.addEventListener('change', () => { this.inputChanged(TypedEvent.nextEventID()); }); } diff --git a/ui/core/components/detailed_results/log_runner.ts b/ui/core/components/detailed_results/log_runner.ts deleted file mode 100644 index 4b1ea38dec..0000000000 --- a/ui/core/components/detailed_results/log_runner.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; - -const layoutHTML = ` -
-
-` -export class LogRunner extends ResultComponent { - private logsContainer: HTMLElement; - - constructor(config: ResultComponentConfig) { - config.rootCssClass = 'log-runner-root'; - super(config) - - this.rootElem.innerHTML = layoutHTML - this.logsContainer = this.rootElem.querySelector('.log-runner-logs') as HTMLElement; - } - - onSimResult(resultData: SimResultData): void { - const logs = resultData.result.logs - this.logsContainer.innerHTML = ''; - logs - .filter(log => { - return !log.isCastCompleted(); - }) - .forEach(log => { - const lineElem = document.createElement('span'); - lineElem.textContent = log.toString(); - this.logsContainer.appendChild(lineElem); - }); - } -} diff --git a/ui/core/components/detailed_results/log_runner.tsx b/ui/core/components/detailed_results/log_runner.tsx new file mode 100644 index 0000000000..0294ef71a5 --- /dev/null +++ b/ui/core/components/detailed_results/log_runner.tsx @@ -0,0 +1,79 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { element, fragment } from 'tsx-vanilla'; + +import { SimLog } from '../../proto_utils/logs_parser.js'; +import { TypedEvent } from '../../typed_event.js'; +import { BooleanPicker } from '../boolean_picker.js'; +import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; + +export class LogRunner extends ResultComponent { + private logsContainer: HTMLElement; + + private showDebug = false; + + readonly showDebugChangeEmitter = new TypedEvent('Show Debug'); + + constructor(config: ResultComponentConfig) { + config.rootCssClass = 'log-runner-root'; + super(config) + + this.rootElem.appendChild( + <> +
+ + + + + + + + +
Time +
Event
+
+ + ) + this.logsContainer = this.rootElem.querySelector('.log-runner-logs')!; + + new BooleanPicker(this.rootElem.querySelector('.show-debug-container')!, this, { + extraCssClasses: ['show-debug-picker'], + label: 'Show Debug Statements', + inline: true, + reverse: true, + changedEvent: () => this.showDebugChangeEmitter, + getValue: () => this.showDebug, + setValue: (eventID, _logRunner, newValue) => { + this.showDebug = newValue; + this.showDebugChangeEmitter.emit(eventID); + } + }); + + this.showDebugChangeEmitter.on(() => this.onSimResult(this.getLastSimResult())); + } + + onSimResult(resultData: SimResultData): void { + const logs = resultData.result.logs + this.logsContainer.innerHTML = ''; + logs. + filter(log => !log.isCastCompleted()). + forEach(log => { + const lineElem = document.createElement('span'); + lineElem.textContent = log.toString(); + if (log.raw.length > 0 && (this.showDebug || !log.raw.match(/.*\[DEBUG\].*/))) { + this.logsContainer.appendChild( + + {log.formattedTimestamp()} + {this.newEventFrom(log)} + + ); + } + }); + } + + private newEventFrom(log: SimLog): Element { + const eventString = log.toString(false).trim(); + const wrapper = ; + wrapper.innerHTML = eventString; + return wrapper; + } +} diff --git a/ui/core/components/detailed_results/timeline.tsx b/ui/core/components/detailed_results/timeline.tsx index 96ada58a4d..866cd4292e 100644 --- a/ui/core/components/detailed_results/timeline.tsx +++ b/ui/core/components/detailed_results/timeline.tsx @@ -1,27 +1,27 @@ +import { Tooltip } from 'bootstrap'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { element, fragment } from 'tsx-vanilla'; + import { ResourceType } from '../../proto/api.js'; import { OtherAction } from '../../proto/common.js'; -import { UnitMetrics } from '../../proto_utils/sim_result.js'; import { ActionId, resourceTypeToIcon } from '../../proto_utils/action_id.js'; -import { resourceNames } from '../../proto_utils/names.js'; -import { orderedResourceTypes } from '../../proto_utils/utils.js'; -import { TypedEvent } from '../../typed_event.js'; -import { bucket, distinct, maxIndex, stringComparator } from '../../utils.js'; - import { AuraUptimeLog, CastLog, - ResourceChangedLogGroup, DpsLog, + ResourceChangedLogGroup, SimLog, ThreatLogGroup, } from '../../proto_utils/logs_parser.js'; - +import { resourceNames } from '../../proto_utils/names.js'; +import { UnitMetrics } from '../../proto_utils/sim_result.js'; +import { orderedResourceTypes } from '../../proto_utils/utils.js'; +import { TypedEvent } from '../../typed_event.js'; +import { bucket, distinct, htmlDecode, maxIndex, stringComparator } from '../../utils.js'; import { actionColors } from './color_settings.js'; import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; -import tippy from 'tippy.js'; -import { element, fragment } from 'tsx-vanilla'; -declare var ApexCharts: any; +declare let ApexCharts: any; type TooltipHandler = (dataPointIndex: number) => string; @@ -131,23 +131,23 @@ export class Timeline extends ResultComponent { let isMouseDown = false let startX = 0; let scrollLeft = 0; - this.rotationTimeline.ondragstart = (event) => { + this.rotationTimeline.ondragstart = event => { event.preventDefault(); } - this.rotationTimeline.onmousedown = (event) => { + this.rotationTimeline.onmousedown = event => { isMouseDown = true; startX = event.pageX - this.rotationTimeline.offsetLeft; scrollLeft = this.rotationTimeline.scrollLeft; } - this.rotationTimeline.onmouseleave = (event) => { + this.rotationTimeline.onmouseleave = () => { isMouseDown = false; this.rotationTimeline.classList.remove('active'); }; - this.rotationTimeline.onmouseup = (event) => { + this.rotationTimeline.onmouseup = () => { isMouseDown = false; this.rotationTimeline.classList.remove('active'); }; - this.rotationTimeline.onmousemove = (e) => { + this.rotationTimeline.onmousemove = e => { if(!isMouseDown) return; e.preventDefault(); const x = e.pageX - this.rotationTimeline.offsetLeft; @@ -170,7 +170,7 @@ export class Timeline extends ResultComponent { } const duration = this.resultData!.result.result.firstIterationDuration || 1; - let options: any = { + const options: any = { series: [], colors: [], xaxis: { @@ -517,7 +517,7 @@ export class Timeline extends ResultComponent { playerCastsByAbility.forEach(castLogs => this.addCastRow(castLogs, buffsAndDebuffsById, duration)); if (player.pets.length > 0) { - let playerPets = new Map(); + const playerPets = new Map(); player.pets.forEach(petsLog => { const petCastsByAbility = this.getSortedCastsByAbility(petsLog); if (petCastsByAbility.length > 0) { @@ -603,7 +603,7 @@ export class Timeline extends ResultComponent { private makeLabelElem(actionId: ActionId, isHiddenLabel: boolean): JSX.Element { const labelText = idsToGroupForRotation.includes(actionId.spellId) ? actionId.baseName : actionId.name; - let labelElem = ( + const labelElem = (
@@ -622,9 +622,11 @@ export class Timeline extends ResultComponent { } this.hiddenIdsChangeEmitter.emit(TypedEvent.nextEventID()); }); - tippy(hideElem, { - content: isHiddenLabel ? 'Show Row' : 'Hide Row', - ignoreAttributes: true, + Tooltip.getOrCreateInstance(hideElem, { + customClass: "timeline-tooltip", + html: true, + placement: 'bottom', + title: isHiddenLabel ? 'Show Row' : 'Hide Row', }); const updateHidden = () => { if (isHiddenLabel == Boolean(this.hiddenIds.find(hiddenId => hiddenId.equals(actionId)))) { @@ -642,7 +644,7 @@ export class Timeline extends ResultComponent { private makeRowElem(actionId: ActionId, duration: number): JSX.Element { const rowElem = ( -
@@ -709,7 +711,7 @@ export class Timeline extends ResultComponent { this.rotationLabels.appendChild(labelElem); const rowElem = ( -
@@ -741,10 +743,10 @@ export class Timeline extends ResultComponent { } rowElem.appendChild(resourceElem); - tippy(resourceElem, { - content: this.resourceTooltipElem(resourceLogGroup, startValue, false), + Tooltip.getOrCreateInstance(resourceElem, { + html: true, placement: 'bottom', - ignoreAttributes: true, + title: this.resourceTooltipElem(resourceLogGroup, startValue, false), }); }); this.rotationTimeline.appendChild(rowElem); @@ -793,25 +795,31 @@ export class Timeline extends ResultComponent { const totalDamage = castLog.totalDamage(); const tt = ( - <> - {castLog.actionId!.name} from {castLog.timestamp.toFixed(2)}s to {(castLog.timestamp + castLog.castTime).toFixed(2)}s ({castLog.castTime.toFixed(2)}s, {castLog.effectiveTime.toFixed(2)}s GCD Time){travelTimeStr} -
    - {castLog.damageDealtLogs.map(ddl => ( -
  • - {ddl.timestamp.toFixed(2)}s - {ddl.resultString()} - {ddl.source?.isTarget && ({ddl.threat.toFixed(1)} Threat)} -
  • - )) - } -
+
+ + {castLog.actionId!.name} from {castLog.timestamp.toFixed(2)}s to {(castLog.timestamp + castLog.castTime).toFixed(2)}s + ({castLog.castTime > 0 && `${castLog.castTime.toFixed(2)}s, `} {castLog.effectiveTime.toFixed(2)}s GCD Time) + {travelTimeStr.length > 0 && travelTimeStr} + + {castLog.damageDealtLogs.length > 0 && ( +
    + {castLog.damageDealtLogs.map(ddl => ( +
  • + {ddl.timestamp.toFixed(2)}s - {htmlDecode(ddl.resultString())} + {ddl.source?.isTarget && ({ddl.threat.toFixed(1)} Threat)} +
  • + )) + } +
+ )} {totalDamage > 0 && Total: {totalDamage.toFixed(2)} ({(totalDamage / (castLog.effectiveTime || 1)).toFixed(2)} DPET)} - +
); - tippy(castElem, { - content: tt, + Tooltip.getOrCreateInstance(castElem, { + html: true, placement: 'bottom', - ignoreAttributes: true, + title: tt.outerHTML, }); castLog.damageDealtLogs.filter(ddl => ddl.tick).forEach(ddl => { @@ -821,16 +829,16 @@ export class Timeline extends ResultComponent { rowElem.appendChild(tickElem); const tt = ( - <> - {ddl.timestamp.toFixed(2)}s - {ddl.actionId!.name} {ddl.resultString()} +
+ {ddl.timestamp.toFixed(2)}s - {ddl.actionId!.name} {htmlDecode(ddl.resultString())} {ddl.source?.isTarget && ({ddl.threat.toFixed(1)} Threat)} - +
); - tippy(tickElem, { - content: tt, + Tooltip.getOrCreateInstance(tickElem, { + html: true, placement: 'bottom', - ignoreAttributes: true, + title: tt.outerHTML, }); }); }); @@ -846,7 +854,7 @@ export class Timeline extends ResultComponent { private addAuraRow(auraUptimeLogs: Array, duration: number) { const actionId = auraUptimeLogs[0].actionId!; - let rowElem = this.makeRowElem(actionId, duration); + const rowElem = this.makeRowElem(actionId, duration); this.rotationLabels.appendChild(this.makeLabelElem(actionId, false)); this.rotationHiddenIdsContainer.appendChild(this.makeLabelElem(actionId, true)); this.rotationTimeline.appendChild(rowElem); @@ -862,11 +870,16 @@ export class Timeline extends ResultComponent { auraElem.style.minWidth = this.timeToPx(aul.fadedAt === aul.gainedAt ? 0.001 : aul.fadedAt - aul.gainedAt); rowElem.appendChild(auraElem); - const tt = ( {aul.actionId!.name}: {aul.gainedAt.toFixed(2)}s - {(aul.fadedAt).toFixed(2)}s); + const tt = ( +
+ {aul.actionId!.name}: {aul.gainedAt.toFixed(2)}s - {(aul.fadedAt).toFixed(2)}s +
+ ); - tippy(auraElem, { - content: tt, - ignoreAttributes: true, + Tooltip.getOrCreateInstance(auraElem, { + html: true, + placement: 'bottom', + title: tt.outerHTML, }); aul.stacksChange.forEach((scl, i) => { @@ -940,24 +953,26 @@ export class Timeline extends ResultComponent { private dpsTooltip(log: DpsLog, includeAuras: boolean, player: UnitMetrics, colorOverride: string): string { const showPlayerLabel = colorOverride != ''; - return `
-
- ${showPlayerLabel ? ` - - ${player.label} - - ` : ''} - ${log.timestamp.toFixed(2)}s -
-
-
    - ${log.damageLogs.map(damageLog => this.tooltipLogItem(damageLog, damageLog.resultString())).join('')} -
-
- DPS: ${log.dps.toFixed(2)} + return ` +
+
+ ${showPlayerLabel ? ` + + ${player.label} - + ` : ''} + ${log.timestamp.toFixed(2)}s +
+
+
    + ${log.damageLogs.map(damageLog => this.tooltipLogItem(damageLog, damageLog.resultString())).join('')} +
+
+ DPS: ${log.dps.toFixed(2)} +
+ ${this.tooltipAurasSection(log)}
- ${this.tooltipAurasSection(log)} -
`; + `; } private threatTooltip(log: ThreatLogGroup, includeAuras: boolean, player: UnitMetrics, colorOverride: string): string { @@ -1009,15 +1024,15 @@ export class Timeline extends ResultComponent { {includeAuras && this.tooltipAurasSectionElem(log)}
); - } - + } + private resourceTooltip(log: ResourceChangedLogGroup, maxValue: number, includeAuras: boolean): string { return this.resourceTooltipElem(log, maxValue, includeAuras).outerHTML; } private tooltipLogItem(log: SimLog, value: string): string { return this.tooltipLogItemElem(log, value).outerHTML; - } + } private tooltipLogItemElem(log: SimLog, value: string): JSX.Element { return ( diff --git a/ui/core/components/individual_sim_ui/rotation_tab.ts b/ui/core/components/individual_sim_ui/rotation_tab.ts index 2bfe14199a..7a5f9fab90 100644 --- a/ui/core/components/individual_sim_ui/rotation_tab.ts +++ b/ui/core/components/individual_sim_ui/rotation_tab.ts @@ -1,30 +1,27 @@ +import * as Tooltips from '../../constants/tooltips.js'; import { IndividualSimUI, InputSection } from "../../individual_sim_ui"; -import { - Spec, -} from "../../proto/common"; +import { Player } from "../../player"; import { APLRotation, APLRotation_Type as APLRotationType, } from "../../proto/apl"; +import { + Spec, +} from "../../proto/common"; import { SavedRotation, } from "../../proto/ui"; import { EventID, TypedEvent } from "../../typed_event"; -import { Player } from "../../player"; - -import { ContentBlock } from "../content_block"; -import { SimTab } from "../sim_tab"; -import { NumberPicker } from "../number_picker"; import { BooleanPicker } from "../boolean_picker"; +import { ContentBlock } from "../content_block"; import { EnumPicker } from "../enum_picker"; +import * as IconInputs from '../icon_inputs.js'; import { Input } from "../input"; -import { CooldownsPicker } from "./cooldowns_picker"; +import { NumberPicker } from "../number_picker"; import { SavedDataManager } from "../saved_data_manager"; - -import * as IconInputs from '../icon_inputs.js'; -import * as Tooltips from '../../constants/tooltips.js'; - +import { SimTab } from "../sim_tab"; import { APLRotationPicker } from "./apl_rotation_picker"; +import { CooldownsPicker } from "./cooldowns_picker"; export class RotationTab extends SimTab { protected simUI: IndividualSimUI; @@ -161,7 +158,7 @@ export class RotationTab extends SimTab { if (inputConfig.type == 'number') { new NumberPicker(sectionElem, this.simUI.player, inputConfig); } else if (inputConfig.type == 'boolean') { - new BooleanPicker(sectionElem, this.simUI.player, { ...inputConfig, ...{ cssScheme: this.simUI.cssScheme } }); + new BooleanPicker(sectionElem, this.simUI.player, { ...inputConfig }); } else if (inputConfig.type == 'enum') { new EnumPicker(sectionElem, this.simUI.player, inputConfig); } diff --git a/ui/core/components/individual_sim_ui/settings_tab.ts b/ui/core/components/individual_sim_ui/settings_tab.ts index e5898adcca..78f678105a 100644 --- a/ui/core/components/individual_sim_ui/settings_tab.ts +++ b/ui/core/components/individual_sim_ui/settings_tab.ts @@ -1,3 +1,4 @@ +import * as Tooltips from '../../constants/tooltips.js'; import { Encounter } from '../../encounter'; import { IndividualSimUI, InputSection } from "../../individual_sim_ui"; import { @@ -16,25 +17,21 @@ import { professionNames, raceNames } from "../../proto_utils/names"; import { specToEligibleRaces } from "../../proto_utils/utils"; import { EventID, TypedEvent } from "../../typed_event"; import { getEnumValues } from "../../utils"; - import { BooleanPicker } from "../boolean_picker"; import { ContentBlock } from "../content_block"; import { EncounterPicker } from '../encounter_picker.js'; import { EnumPicker } from "../enum_picker"; +import * as IconInputs from '../icon_inputs.js'; import { Input } from "../input"; +import * as BuffDebuffInputs from '../inputs/buffs_debuffs'; import { relevantStatOptions } from "../inputs/stat_options"; import { ItemSwapPicker } from "../item_swap_picker"; import { MultiIconPicker } from "../multi_icon_picker"; import { NumberPicker } from "../number_picker"; import { SavedDataManager } from "../saved_data_manager"; import { SimTab } from "../sim_tab"; - import { ConsumesPicker } from "./consumes_picker"; -import * as IconInputs from '../icon_inputs.js'; -import * as BuffDebuffInputs from '../inputs/buffs_debuffs'; -import * as Tooltips from '../../constants/tooltips.js'; - export class SettingsTab extends SimTab { protected simUI: IndividualSimUI; @@ -134,7 +131,7 @@ export class SettingsTab extends SimTab { this.configureInputSection(contentBlock.bodyElement, this.simUI.individualConfig.playerInputs); } - let professionGroup = Input.newGroupContainer(); + const professionGroup = Input.newGroupContainer(); contentBlock.bodyElement.appendChild(professionGroup); const professions = getEnumValues(Profession) as Array; @@ -167,7 +164,7 @@ export class SettingsTab extends SimTab { private buildCustomSettingsSections() { (this.simUI.individualConfig.customSections || []).forEach(customSection => { - let section = customSection(this.column2, this.simUI); + const section = customSection(this.column2, this.simUI); section.rootElem.classList.add('custom-section'); }); } @@ -238,7 +235,7 @@ export class SettingsTab extends SimTab { debuffOptions.map(options => options.picker && new options.picker(contentBlock.bodyElement, this.simUI.player, options.config as any, this.simUI)) ); - const miscDebuffOptions = relevantStatOptions(BuffDebuffInputs.DEBUFFS_MISC_CONFIG, this.simUI) + const miscDebuffOptions = relevantStatOptions(BuffDebuffInputs.DEBUFFS_MISC_CONFIG, this.simUI) if (miscDebuffOptions.length) { new MultiIconPicker(contentBlock.bodyElement, this.simUI.player, { inputs: miscDebuffOptions.map(options => options.config), @@ -336,7 +333,7 @@ export class SettingsTab extends SimTab { if (inputConfig.type == 'number') { new NumberPicker(sectionElem, this.simUI.player, inputConfig); } else if (inputConfig.type == 'boolean') { - new BooleanPicker(sectionElem, this.simUI.player, { ...inputConfig, ...{ cssScheme: this.simUI.cssScheme } }); + new BooleanPicker(sectionElem, this.simUI.player, { ...inputConfig }); } else if (inputConfig.type == 'enum') { new EnumPicker(sectionElem, this.simUI.player, inputConfig); } diff --git a/ui/core/proto_utils/logs_parser.ts b/ui/core/proto_utils/logs_parser.tsx similarity index 82% rename from ui/core/proto_utils/logs_parser.ts rename to ui/core/proto_utils/logs_parser.tsx index 87ae32418b..351c7305e4 100644 --- a/ui/core/proto_utils/logs_parser.ts +++ b/ui/core/proto_utils/logs_parser.tsx @@ -1,8 +1,10 @@ -import { RaidSimRequest, RaidSimResult } from '../proto/api.js'; -import { ResourceType } from '../proto/api.js'; -import { ActionId } from '../proto_utils/action_id.js'; -import { resourceNames, stringToResourceType } from '../proto_utils/names.js'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { element } from 'tsx-vanilla'; + +import { RaidSimResult , ResourceType } from '../proto/api.js'; import { bucket, getEnumValues, stringComparator, sum } from '../utils.js'; +import { ActionId } from './action_id.js'; +import { resourceNames, stringToResourceType } from './names.js'; export class Entity { readonly name: string; @@ -29,7 +31,7 @@ export class Entity { toString(): string { if (this.isTarget) { - return 'Target ' + (this.index + 1); + return `Target ${this.index + 1}`; } else if (this.isPet) { return `${this.ownerName} (#${this.index + 1}) - ${this.name}`; } else { @@ -37,6 +39,16 @@ export class Entity { } } + toHTMLString(): string { + if (this.isTarget) { + return `[Target ${this.index + 1}]`; + } else if (this.isPet) { + return `[${this.ownerName} ${this.index + 1}] - ${this.name}`; + } else { + return `[${this.name} ${this.index + 1}]`; + } + } + // Parses one or more Entities from a string. // Each entity label should be one of: // 'Target 1' if a target, @@ -101,17 +113,61 @@ export class SimLog { this.activeAuras = []; } - toString(): string { - return this.raw; + toString(includeTimestamp = true): string { + let str = this.raw; + // Base logs already have the timestamp appended by default + if (!includeTimestamp) { + const regexp = /(\[[0-9.-]+\]) (\[[0-9a-zA-Z\s\-()#]+\])?(.*)/; + if (this.raw.match(regexp)) { + // TypeScript doesn't handle regex capture typing well + const captureArr = regexp.exec(this.raw); + // const timestamp = captureArr[1]; + // const source = captureArr[2]; + + if (captureArr && captureArr.length == 4) { + str = captureArr[3]; + } + } + } + + if (this.source) { + str = `${this.source.toHTMLString()} ${str}`; + } + + return str; } - toStringPrefix(): string { - const timestampStr = `[${this.timestamp.toFixed(2)}]`; + toStringPrefix(includeTimestamp = true): string { + let prefix = ''; + if (includeTimestamp) { + prefix = `[${this.timestamp.toFixed(2)}]`; + } if (this.source) { - return `${timestampStr} [${this.source}]`; - } else { - return timestampStr; + prefix = `${prefix} ${this.source.toHTMLString()}`; } + + return prefix; + } + + formattedTimestamp(): string { + const positiveTimestamp = Math.abs(this.timestamp); + const minutes = Math.floor(positiveTimestamp / 60); + const seconds = Math.floor(positiveTimestamp - minutes * 60); + const milliseconds = ((positiveTimestamp - Math.floor(positiveTimestamp)) * 1000).toFixed(); + + let formatted = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(milliseconds).padStart(3, '0')}`; + if (this.timestamp < 0) { + formatted = `-${formatted}` + } + return formatted + } + + protected newActionIdLink(): string { + const iconElem = ; + const actionAnchor = {iconElem} {this.actionId!.name}; + this.actionId?.setBackground(iconElem as HTMLAnchorElement); + this.actionId?.setWowheadHref(actionAnchor as HTMLAnchorElement); + return actionAnchor.outerHTML; } static async parseAll(result: RaidSimResult): Promise> { @@ -134,13 +190,13 @@ export class SimLog { line = line.substring(0, threatMatch.index); } - let match = line.match(/\[(-?[0-9]+\.[0-9]+)\]\w*(.*)/); + const match = line.match(/\[(-?[0-9]+\.[0-9]+)\]\w*(.*)/); if (!match || !match[1]) { return new SimLog(params); } params.timestamp = parseFloat(match[1]); - let remainder = match[2]; + const remainder = match[2]; const entities = Entity.parseAll(remainder); params.source = entities[0] || null; @@ -277,9 +333,9 @@ export class DamageDealtLog extends SimLog { : this.tick ? 'Tick' : 'Hit'; - result += ' ' + this.target; + result += ' ' + this.target?.toHTMLString(); if (!this.miss && !this.dodge && !this.parry) { - result += ` for ${this.amount.toFixed(2)}`; + result += ` for ${this.amount.toFixed(2)} damage`; if (this.partialResist1_4) { result += ' (25% Resist)'; } else if (this.partialResist2_4) { @@ -292,9 +348,9 @@ export class DamageDealtLog extends SimLog { return result; } - toString(): string { + toString(includeTimestamp = true): string { const threatPostfix = this.source?.isTarget ? '' : ` (${this.threat.toFixed(2)} Threat)`; - return `${this.toStringPrefix()} ${this.actionId!.name} ${this.resultString()}${threatPostfix}`; + return `${this.toStringPrefix(includeTimestamp)} ${this.newActionIdLink()} ${this.resultString()}${threatPostfix}`; } static parse(params: SimLogParams): Promise | null { @@ -433,8 +489,8 @@ export class AuraEventLog extends SimLog { this.isRefreshed = isRefreshed; } - toString(): string { - return `${this.toStringPrefix()} Aura ${this.isGained ? 'gained' : this.isFaded ? 'faded' : 'refreshed'}: ${this.actionId!.name}.`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} Aura ${this.isGained ? 'gained' : this.isFaded ? 'faded' : 'refreshed'}: ${this.newActionIdLink()}.`; } static parse(params: SimLogParams): Promise | null { @@ -461,8 +517,8 @@ export class AuraStacksChangeLog extends SimLog { this.newStacks = newStacks; } - toString(): string { - return `${this.toStringPrefix()} ${this.actionId!.name} stacks: ${this.oldStacks} --> ${this.newStacks}.`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} ${this.newActionIdLink()} stacks: ${this.oldStacks} → ${this.newStacks}.`; } static parse(params: SimLogParams): Promise | null { @@ -491,7 +547,7 @@ export class AuraUptimeLog extends SimLog { } static fromLogs(logs: Array, entity: Entity, encounterDuration: number): Array { - let unmatchedGainedLogs: Array<{ gained: AuraEventLog, stacks: Array }> = []; + const unmatchedGainedLogs: Array<{ gained: AuraEventLog, stacks: Array }> = []; const uptimeLogs: Array = []; logs.forEach((log: SimLog) => { @@ -591,13 +647,15 @@ export class ResourceChangedLog extends SimLog { this.isSpend = isSpend; } - toString(): string { + toString(includeTimestamp = true): string { const signedDiff = (this.valueAfter - this.valueBefore) * (this.isSpend ? -1 : 1); const isHealth = this.resourceType == ResourceType.ResourceTypeHealth; const verb = isHealth ? (this.isSpend ? 'Lost' : 'Recovered') : (this.isSpend ? 'Spent' : 'Gained'); + const resourceName = resourceNames.get(this.resourceType)! + const resourceKlass = `resource-${resourceName.replace(/\s/g, "-").toLowerCase()}`; - return `${this.toStringPrefix()} ${verb} ${signedDiff.toFixed(1)} ${resourceNames.get(this.resourceType)} from ${this.actionId!.name}. (${this.valueBefore.toFixed(1)} --> ${this.valueAfter.toFixed(1)})`; + return `${this.toStringPrefix(includeTimestamp)} ${verb} ${signedDiff.toFixed(1)} ${resourceName} from ${this.newActionIdLink()}. (${this.valueBefore.toFixed(1)} → ${this.valueAfter.toFixed(1)})`; } resultString(): string { @@ -610,12 +668,12 @@ export class ResourceChangedLog extends SimLog { } static parse(params: SimLogParams): Promise | null { - const match = params.raw.match(/((Gained)|(Spent)) \d+\.?\d* ((health)|(mana)|(energy)|(focus)|(rage)|(combo points)|(runic power)|(blood rune)|(frost rune)|(unholy rune)|(death rune)) from (.*) \((\d+\.?\d*) --> (\d+\.?\d*)\)/); + const match = params.raw.match(/((Gained)|(Spent)) \d+\.?\d* ((health)|(mana)|(energy)|(focus)|(rage)|(combo points)) from (.*) \((\d+\.?\d*) --> (\d+\.?\d*)\)/); if (match) { const resourceType = stringToResourceType(match[4]); - return ActionId.fromLogString(match[16]).fill(params.source?.index).then(cause => { + return ActionId.fromLogString(match[11]).fill(params.source?.index).then(cause => { params.actionId = cause; - return new ResourceChangedLog(params, resourceType, parseFloat(match[17]), parseFloat(match[18]), match[1] == 'Spent'); + return new ResourceChangedLog(params, resourceType, parseFloat(match[12]), parseFloat(match[13]), match[1] == 'Spent'); }); } else { return null; @@ -637,8 +695,8 @@ export class ResourceChangedLogGroup extends SimLog { this.logs = logs; } - toString(): string { - return `${this.toStringPrefix()} ${resourceNames.get(this.resourceType)}: ${this.valueBefore.toFixed(1)} --> ${this.valueAfter.toFixed(1)}`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} ${resourceNames.get(this.resourceType)}: ${this.valueBefore.toFixed(1)} → ${this.valueAfter.toFixed(1)}`; } static fromLogs(logs: Array): Record> { @@ -675,8 +733,8 @@ export class MajorCooldownUsedLog extends SimLog { super(params); } - toString(): string { - return `${this.toStringPrefix()} Major cooldown used: ${this.actionId!.name}.`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} Major cooldown used: ${this.newActionIdLink()}.`; } static parse(params: SimLogParams): Promise | null { @@ -704,8 +762,8 @@ export class CastBeganLog extends SimLog { this.effectiveTime = effectiveTime; } - toString(): string { - return `${this.toStringPrefix()} Casting ${this.actionId!.name} (Cast time = ${this.castTime.toFixed(2)}s, Cost = ${this.manaCost.toFixed(1)}).`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} Casting ${this.newActionIdLink()} (Cast time: ${this.castTime.toFixed(2)}s, Cost: ${this.manaCost.toFixed(1)} Mana).`; } static parse(params: SimLogParams): Promise | null { @@ -734,8 +792,8 @@ export class CastCompletedLog extends SimLog { super(params); } - toString(): string { - return `${this.toStringPrefix()} Completed cast ${this.actionId!.name}.`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} Completed cast ${this.actionId!.name}.`; } static parse(params: SimLogParams): Promise | null { @@ -778,6 +836,11 @@ export class CastLog extends SimLog { this.castCompletedLog = castCompletedLog; this.damageDealtLogs = damageDealtLogs; + if (this.castCompletedLog && this.castBeganLog) { + this.castTime = this.castCompletedLog.timestamp - this.castBeganLog.timestamp + this.effectiveTime = this.castCompletedLog.timestamp - this.castBeganLog.timestamp + } + if (this.castCompletedLog && this.damageDealtLogs.length == 1 && this.castCompletedLog.timestamp < this.damageDealtLogs[0].timestamp && !this.damageDealtLogs[0].tick) { @@ -787,8 +850,8 @@ export class CastLog extends SimLog { } } - toString(): string { - return `${this.toStringPrefix()} Casting ${this.actionId!.name} (Cast time = ${this.castTime.toFixed(2)}s).`; + toString(includeTimestamp = true): string { + return `${this.toStringPrefix(includeTimestamp)} Casting ${this.actionId!.name} (Cast time = ${this.castTime.toFixed(2)}s).`; } totalDamage(): number { @@ -835,7 +898,7 @@ export class CastLog extends SimLog { } // Find all damage dealt logs between the cur and next cast completed logs. - let ddLogs = []; + const ddLogs = []; while (abilityDamageDealt && ddIdx < abilityDamageDealt.length && (!nextCcLog || abilityDamageDealt[ddIdx].timestamp < nextCcLog.timestamp)) { ddLogs.push(abilityDamageDealt[ddIdx]); ddIdx++ @@ -859,11 +922,11 @@ export class StatChangeLog extends SimLog { this.stats = stats; } - toString(): string { + toString(includeTimestamp = true): string { if (this.isGain) { - return `${this.toStringPrefix()} Gained ${this.stats} from ${this.actionId!.name}.`; + return `${this.toStringPrefix(includeTimestamp)} Gained ${this.stats} from ${this.newActionIdLink()}.`; } else { - return `${this.toStringPrefix()} Lost ${this.stats} from fading ${this.actionId!.name}.`; + return `${this.toStringPrefix(includeTimestamp)} Lost ${this.stats} from fading ${this.newActionIdLink()}.`; } } diff --git a/ui/core/utils.ts b/ui/core/utils.ts index d5e0d12b5a..562b1e8925 100644 --- a/ui/core/utils.ts +++ b/ui/core/utils.ts @@ -28,7 +28,7 @@ export function sortByProperty(objArray: any[], prop: string) { const direct = arguments.length > 2 ? arguments[2] : 1; //Default to ascending const propPath = (prop.constructor === Array) ? prop : prop.split('.'); clone.sort(function(a, b) { - for (let p in propPath) { + for (const p in propPath) { if (a[propPath[p]] && b[propPath[p]]) { a = a[propPath[p]]; b = b[propPath[p]]; @@ -269,4 +269,9 @@ function jsonStringifyCustomHelper(value: any, indentStr: string, path: Array) => boolean): string { return jsonStringifyCustom(value, indent, (value, path) => handler(value, path) ? JSON.stringify(value) : undefined); -} \ No newline at end of file +} + +export function htmlDecode(input: string) { + const doc = new DOMParser().parseFromString(input, "text/html"); + return doc.documentElement.textContent; +} diff --git a/ui/scss/core/components/_detailed_results.scss b/ui/scss/core/components/_detailed_results.scss index 4f85a88fc1..bebf5c1563 100644 --- a/ui/scss/core/components/_detailed_results.scss +++ b/ui/scss/core/components/_detailed_results.scss @@ -146,19 +146,6 @@ .metrics-table { width: 100%; - font-size: 16px; -} - -.melee-metrics-root .metrics-table { - font-size: 12px; -} - -.hide-in-front-of-target.melee-metrics-root .metrics-table, .hide-threat-metrics .melee-metrics-root .metrics-table { - font-size: 14px; -} - -.hide-threat-metrics .hide-in-front-of-target.melee-metrics-root .metrics-table { - font-size: 16px; } .metrics-table-header-row { diff --git a/ui/scss/core/components/_icon_picker.scss b/ui/scss/core/components/_icon_picker.scss index 07cfac71fe..40183b221b 100644 --- a/ui/scss/core/components/_icon_picker.scss +++ b/ui/scss/core/components/_icon_picker.scss @@ -8,22 +8,19 @@ .icon-picker-button, .icon-dropdown-option { - min-width: $icon-size-md; - width: $icon-size-md; - height: $icon-size-md; - border: $border-default; + @extend .icon-md; + + border: $border-default; transition: border-color .15s ease-in-out; - background-size: cover; + position: relative; + display: block; + background-color: grey; filter: grayscale(1); &.active { border-color: $success; filter: none; } - - .gem-socket-container { - --gem-width: 0.8rem; - } } .icon-picker-button, @@ -106,7 +103,7 @@ &> * { margin-bottom: $block-spacer; flex: 0; - + &:not(:last-child) { margin-right: $block-spacer; } diff --git a/ui/scss/core/components/detailed_results/_log_runner.scss b/ui/scss/core/components/detailed_results/_log_runner.scss index 7f6c04e370..ff4f6e6959 100644 --- a/ui/scss/core/components/detailed_results/_log_runner.scss +++ b/ui/scss/core/components/detailed_results/_log_runner.scss @@ -1,8 +1,59 @@ .log-runner-root { - display: flex; - flex-direction: column; -} + .log-runner-table { + th { + padding: map-get($spacers, 2); + vertical-align: bottom; + + &:not(:first-child) { + padding-left: 0; + } + } + + .log-runner-logs { + tr { + &:not(:last-child) { + td { + border-bottom: 1px solid $border-color; + } + } + + td { + padding-top: map-get($spacers, 2); + padding-bottom: map-get($spacers, 2); + } + + .log-timestamp { + padding-right: map-get($spacers, 2); + text-align: right; + vertical-align: top; + font-variant-numeric: tabular-nums; + } + + .log-event { + // Fill any extra width + width: 100%; + + .icon { + vertical-align: middle; + } + + .log-action { + color: $brand; + } + } + } + } + } + + .show-debug-container { + display: flex; + + .show-debug-picker { + width: unset; -.log-runner-logs > span { - display: block; + label { + margin-left: map-get($spacers, 1); + } + } + } } diff --git a/ui/scss/core/components/detailed_results/_timeline.scss b/ui/scss/core/components/detailed_results/_timeline.scss index deadfcd057..b2456884d2 100644 --- a/ui/scss/core/components/detailed_results/_timeline.scss +++ b/ui/scss/core/components/detailed_results/_timeline.scss @@ -1,11 +1,5 @@ $dps-color: #ed5653; $threat-color: #b56d07; -$health-color: #22ba00; -$mana-color: #2E93fA; -$energy-color: #ffd700; -$rage-color: #ff0000; -$focus-color: #cd853f; -$combo-points-color: #ffa07a; .threat .series-color, .threat.series-color { color: $threat-color; @@ -73,10 +67,7 @@ $combo-points-color: #ffa07a; } .timeline-tooltip { - background-color: #333; - border: none; - color: white; - padding: 3px; + text-align: start; } .timeline-tooltip-header { @@ -94,43 +85,6 @@ $combo-points-color: #ffa07a; height: 20px; } -.timeline-tooltip ul { // Remove default bullets - list-style: none; - padding-left: 1em; -} -.timeline-tooltip ul li::before { // Add our own bullets (so we can have separate color). - content: "\2022"; - font-weight: bold; - font-size: 16px; - display: inline-block; - width: 1em; - margin-left: -1em; -} -.timeline-tooltip.dps ul li::before { - color: $dps-color; -} -.timeline-tooltip.threat ul li::before { - color: $threat-color; -} -.timeline-tooltip.health ul li::before { - color: $health-color; -} -.timeline-tooltip.mana ul li::before { - color: $mana-color; -} -.timeline-tooltip.energy ul li::before { - color: $energy-color; -} -.timeline-tooltip.rage ul li::before { - color: $rage-color; -} -.timeline-tooltip.focus ul li::before { - color: $focus-color; -} -.timeline-tooltip.combo-points ul li::before { - color: $combo-points-color; -} - .series-color { font-weight: bold; } @@ -139,9 +93,6 @@ $combo-points-color: #ffa07a; border-top: 1px solid white; margin-top: 5px; } -.timeline-tooltip-auras ul li::before { - color: white; -} .rotation-container { color: white; diff --git a/ui/scss/shared/_global.scss b/ui/scss/shared/_global.scss index ff38f256ec..9681f8aede 100644 --- a/ui/scss/shared/_global.scss +++ b/ui/scss/shared/_global.scss @@ -101,10 +101,32 @@ kbd { display: none !important; } +.icon-sm { + display: inline-block; + min-width: $icon-size-sm; + width: $icon-size-sm; + height: $icon-size-sm; + background-size: cover; +} + +.icon-md { + display: inline-block; + min-width: $icon-size-md; + width: $icon-size-md; + height: $icon-size-md; + background-size: cover; +} + @each $label, $value in $item-qualities { - .item-quality-#{$label} { - color: $value !important; - } + .item-quality-#{$label} { + color: $value !important; + } +} + +@each $label, $value in $resource-colors { + .resource-#{$label} { + color: $value !important; + } } [contenteditable="true"]:active, diff --git a/ui/scss/shared/_variables.scss b/ui/scss/shared/_variables.scss index 0f3492817a..934d9e272b 100644 --- a/ui/scss/shared/_variables.scss +++ b/ui/scss/shared/_variables.scss @@ -65,14 +65,30 @@ $item-quality-artifact: #e5cc80; $item-quality-heirloom: #0cf; $item-qualities: ( - junk: $item-quality-junk, - common: $item-quality-common, - uncommon: $item-quality-uncommon, - rare: $item-quality-rare, - epic: $item-quality-epic, - legendary: $item-quality-legendary, - artifact: $item-quality-artifact, - heirloom: $item-quality-heirloom + junk: $item-quality-junk, + common: $item-quality-common, + uncommon: $item-quality-uncommon, + rare: $item-quality-rare, + epic: $item-quality-epic, + legendary: $item-quality-legendary, + artifact: $item-quality-artifact, + heirloom: $item-quality-heirloom +); + +$health-color: #22ba00; +$mana-color: #2E93fA; +$energy-color: #ffd700; +$rage-color: #ff0000; +$focus-color: #cd853f; +$combo-points-color:#ffa07a; + +$resource-colors: ( + combo-points: $combo-points-color, + energy: $energy-color, + focus: $focus-color, + health: $health-color, + mana: $mana-color, + rage: $rage-color, ); $link-danger-color: #ef9eaa;