From 35137ba4fbad6f9cc87d5f78ddd9ec374d076971 Mon Sep 17 00:00:00 2001 From: Adrienne Walker Date: Wed, 25 Oct 2023 15:12:42 -0700 Subject: [PATCH] raidboss: Add Option to Order Timeline Bottom-to-Top (#5869) This is #4811 with conflicts resolved due to refactoring and 3ae3f6c1eca6dd6a96d515d7d54ed654f5e9642e on top. 3ae3f6c1eca6dd6a96d515d7d54ed654f5e9642e moves the keep alive logic from html_timeline_ui to timeline so that it knows about how many bars are still on screen and also knows when a bar has been expired and it should consider adding more to the list. Closes #4811. --------- Co-authored-by: Panic Stevenson --- ui/raidboss/html_timeline_ui.ts | 30 +++++++------------- ui/raidboss/raidboss.css | 6 +++- ui/raidboss/raidboss_config.ts | 8 ++++++ ui/raidboss/raidboss_options.ts | 1 + ui/raidboss/timeline.ts | 50 +++++++++++++++++++++++++++++---- 5 files changed, 69 insertions(+), 26 deletions(-) diff --git a/ui/raidboss/html_timeline_ui.ts b/ui/raidboss/html_timeline_ui.ts index 8fbc5a1f46..6f74bffbd6 100644 --- a/ui/raidboss/html_timeline_ui.ts +++ b/ui/raidboss/html_timeline_ui.ts @@ -76,7 +76,6 @@ const computeBackgroundFrom = (element: HTMLElement, classList: string): string export type ActiveBar = { bar: TimerBar; soonTimeout?: number; - expireTimeout?: number; }; export class HTMLTimelineUI extends TimelineUI { @@ -123,6 +122,8 @@ export class HTMLTimelineUI extends TimelineUI { if (this.timerlist) { this.timerlist.style.gridTemplateRows = `repeat(${this.options.MaxNumberOfTimerBars}, min-content)`; + if (this.options.ReverseTimeline) + this.timerlist.classList.add('reversed'); } this.activeBars = {}; @@ -197,11 +198,6 @@ export class HTMLTimelineUI extends TimelineUI { if (activeBar) { const parentDiv = activeBar.bar.parentNode; parentDiv?.parentNode?.removeChild(parentDiv); - // Expiry timeout must be cleared so that it will not remove this new bar. - if (activeBar.expireTimeout !== undefined) { - window.clearTimeout(activeBar.expireTimeout); - activeBar.expireTimeout = undefined; - } // Soon timeout is just an optimization to remove, as it's unnecessary. if (activeBar.soonTimeout !== undefined) { window.clearTimeout(activeBar.soonTimeout); @@ -220,8 +216,10 @@ export class HTMLTimelineUI extends TimelineUI { bar.fg = this.barExpiresSoonColor; } - if (e.sortKey) - div.style.order = e.sortKey.toString(); + if (e.sortKey) { + // Invert the order if the timer bars should "grow" in the reverse direction + div.style.order = ((this.options.ReverseTimeline ? -1 : 1) * e.sortKey).toString(); + } this.timerlist?.appendChild(div); this.activeBars[e.id] = { bar: bar, @@ -229,22 +227,11 @@ export class HTMLTimelineUI extends TimelineUI { }; } - public override OnRemoveTimer(e: Event, expired: boolean, force = false): void { + public override OnRemoveTimer(e: Event, force: boolean): void { const activeBar = this.activeBars[e.id]; if (!activeBar) return; - if (activeBar.expireTimeout !== undefined) - window.clearTimeout(activeBar.expireTimeout); - - if (!force && expired && this.options.KeepExpiredTimerBarsForSeconds) { - activeBar.expireTimeout = window.setTimeout( - () => this.OnRemoveTimer(e, false), - this.options.KeepExpiredTimerBarsForSeconds * 1000, - ); - return; - } - const div = activeBar.bar.parentNode; if (!(div instanceof HTMLElement)) throw new UnreachableCode(); @@ -314,6 +301,9 @@ export class HTMLTimelineUI extends TimelineUI { this.debugFightTimer.stylefill = 'fill'; this.debugFightTimer.bg = 'transparent'; this.debugFightTimer.fg = 'transparent'; + // Align it to the 'first' item in the timeline container + if (this.options.ReverseTimeline) + this.debugElement.classList.add('reversed'); this.debugElement.appendChild(this.debugFightTimer); } diff --git a/ui/raidboss/raidboss.css b/ui/raidboss/raidboss.css index 4f1d0eccc0..480d971865 100644 --- a/ui/raidboss/raidboss.css +++ b/ui/raidboss/raidboss.css @@ -161,10 +161,14 @@ #timeline-debug { position: absolute; - top: 0; left: calc(100%); } +.reversed { + position: absolute; + bottom: 0; +} + .autoplay-helper-button { position: absolute; top: 0; diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index d3948ad921..f5555b2c50 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -2134,6 +2134,14 @@ const templateOptions: OptionsTemplate = { type: 'checkbox', default: true, }, + { + id: 'ReverseTimeline', + name: { + en: 'Reverse timeline order (bottom-to-top)', + }, + type: 'checkbox', + default: false, + }, { id: 'ShowTimerBarsAtSeconds', name: { diff --git a/ui/raidboss/raidboss_options.ts b/ui/raidboss/raidboss_options.ts index f510001025..6f6f144be2 100644 --- a/ui/raidboss/raidboss_options.ts +++ b/ui/raidboss/raidboss_options.ts @@ -101,6 +101,7 @@ const defaultRaidbossConfigOptions = { KeepExpiredTimerBarsForSeconds: 0.7, BarExpiresSoonSeconds: 6, MaxNumberOfTimerBars: 6, + ReverseTimeline: false, DisplayAlarmTextForSeconds: 3, DisplayAlertTextForSeconds: 3, DisplayInfoTextForSeconds: 3, diff --git a/ui/raidboss/timeline.ts b/ui/raidboss/timeline.ts index e0c2c998cb..fbf561a952 100644 --- a/ui/raidboss/timeline.ts +++ b/ui/raidboss/timeline.ts @@ -87,7 +87,7 @@ export class TimelineUI { /* noop */ } - public OnRemoveTimer(_e: Event, _expired: boolean, _force = false): void { + public OnRemoveTimer(_e: Event, _force = false): void { /* noop */ } @@ -135,6 +135,10 @@ export class Timeline { protected activeSyncs: Sync[]; private activeEvents: Event[]; + private keepAliveEvents: { + event: Event; + timeout: number; + }[]; private activeLastForceJumpSync?: Sync; public ignores: { [ignoreId: string]: boolean }; @@ -179,6 +183,8 @@ export class Timeline { this.activeSyncs = []; // Sorted by event occurrence time. this.activeEvents = []; + // Events that are no longer active but we are keeping on screen briefly. + this.keepAliveEvents = []; // A set of names which will not be notified about. this.ignores = {}; // Sorted by event occurrence time. @@ -333,6 +339,10 @@ export class Timeline { for (const activeEvent of this.activeEvents) this.ui?.OnRemoveTimer(activeEvent, false); this.activeEvents = []; + for (const keepAlive of this.keepAliveEvents) { + window.clearTimeout(keepAlive.timeout); + this.ui?.OnRemoveTimer(keepAlive.event, false); + } } private _ClearExceptRunningDurationTimers(fightNow: number): void { @@ -342,8 +352,10 @@ export class Timeline { durationEvents.push(event); continue; } - this.ui?.OnRemoveTimer(event, false, true); + this.ui?.OnRemoveTimer(event, true); } + // Do not clear keep alive events here, as this is part of a sync jump + // and keep alive timing is independent of timeline time. this.activeEvents = durationEvents; } @@ -351,7 +363,35 @@ export class Timeline { private _RemoveExpiredTimers(fightNow: number): void { let activeEvent = this.activeEvents[0]; while (this.activeEvents.length && activeEvent && activeEvent.time <= fightNow) { - this.ui?.OnRemoveTimer(activeEvent, true); + const event = activeEvent; + if (this.options.KeepExpiredTimerBarsForSeconds > 0) { + this.keepAliveEvents.push({ + event: event, + timeout: window.setTimeout( + () => { + // Find and remove the first keepalive event with this id. + let found = false; + this.keepAliveEvents = this.keepAliveEvents.filter((x) => { + if (found) + return true; + if (x.event.id === event.id) { + found = true; + return false; + } + return true; + }); + this.ui?.OnRemoveTimer(event, false); + // Because keepalive events are in "real time" just update the timer + // whenever any has been removed in case more bars need to be added. + this._OnUpdateTimer(Date.now()); + }, + this.options.KeepExpiredTimerBarsForSeconds * 1000, + ), + }); + } else { + this.ui?.OnRemoveTimer(activeEvent, false); + } + this.activeEvents.splice(0, 1); activeEvent = this.activeEvents[0]; } @@ -383,7 +423,7 @@ export class Timeline { }; events.push(durationEvent); this.activeEvents.splice(i, 1); - this.ui?.OnRemoveTimer(e, false, true); + this.ui?.OnRemoveTimer(e, true); this.ui?.OnAddTimer(fightNow, durationEvent, true); --i; } @@ -398,7 +438,7 @@ export class Timeline { private _AddUpcomingTimers(fightNow: number): void { while ( this.nextEventState.index < this.events.length && - this.activeEvents.length < this.options.MaxNumberOfTimerBars + this.activeEvents.length + this.keepAliveEvents.length < this.options.MaxNumberOfTimerBars ) { const e = this.events[this.nextEventState.index]; if (e === undefined)