diff --git a/plugins/ui/static_components/race/TimerWidget.component.ts b/plugins/ui/static_components/race/TimerWidget.component.ts
index f0da3847..eb62be37 100644
--- a/plugins/ui/static_components/race/TimerWidget.component.ts
+++ b/plugins/ui/static_components/race/TimerWidget.component.ts
@@ -10,8 +10,10 @@ export default class TimerWidget extends StaticComponent {
private readonly header: StaticHeader
private dynamicTimerInterval: NodeJS.Timeout | undefined
- private noButtonXml: string = ''
- private xmlWithButtons: string = ''
+ private headerXmlNoButtons: string = ''
+ private headerXmlWithButtons: string = ''
+ private timeXml: string = ''
+ private readonly timeWidgetId = this.id + 4
private readonly pauseButtonId = this.id + 1
private readonly addButtonid = this.id + 2
private readonly subtractButtonId = this.id + 3
@@ -21,8 +23,9 @@ export default class TimerWidget extends StaticComponent {
constructor() {
super(componentIds.timer)
this.header = new StaticHeader('race')
- this.noButtonXml = this.constructXml(false)
- this.xmlWithButtons = this.constructXml(true)
+ this.headerXmlNoButtons = this.constructHeaderXml(false)
+ this.headerXmlWithButtons = this.constructHeaderXml(true)
+ this.timeXml = this.constructTimeXml()
if (tm.timer.isDynamic) {
this.startDynamicTimerInterval()
}
@@ -32,22 +35,25 @@ export default class TimerWidget extends StaticComponent {
} else {
clearInterval(this.dynamicTimerInterval)
}
- this.noButtonXml = this.constructXml(false)
- this.xmlWithButtons = this.constructXml(true)
+ this.headerXmlNoButtons = this.constructHeaderXml(false)
+ this.headerXmlWithButtons = this.constructHeaderXml(true)
+ this.timeXml = this.constructTimeXml()
return this.display()
})
this.renderOnEvent('EndMap', (info) => {
if (info.isRestart) {
this.isOnRestart = true
- this.noButtonXml = this.constructXml(false)
- this.xmlWithButtons = this.constructXml(true)
+ this.headerXmlNoButtons = this.constructHeaderXml(false)
+ this.headerXmlWithButtons = this.constructHeaderXml(true)
+ this.timeXml = this.constructTimeXml()
return this.display()
}
})
this.renderOnEvent('BeginMap', () => {
this.isOnRestart = false
- this.noButtonXml = this.constructXml(false)
- this.xmlWithButtons = this.constructXml(true)
+ this.headerXmlNoButtons = this.constructHeaderXml(false)
+ this.headerXmlWithButtons = this.constructHeaderXml(true)
+ this.timeXml = this.constructTimeXml()
return this.display()
})
addManialinkListener(this.pauseButtonId, (info) => {
@@ -67,6 +73,9 @@ export default class TimerWidget extends StaticComponent {
tm.sendMessage(tm.utils.strVar(config.pause, strObject))
tm.timer.pause()
}
+ this.headerXmlWithButtons = this.constructHeaderXml(true)
+ this.timeXml = this.constructTimeXml()
+ this.sendMultipleManialinks(this.display())
})
addManialinkListener(this.addButtonid, (info) => {
if (info.privilege < config.timerActionsPrivilege) { return }
@@ -81,6 +90,8 @@ export default class TimerWidget extends StaticComponent {
time: tm.utils.getVerboseTime(tm.timer.remainingRaceTime)
}
tm.sendMessage(tm.utils.strVar(config.set, strObject))
+ this.timeXml = this.constructTimeXml()
+ tm.sendManialink(this.timeXml)
})
addManialinkListener(this.subtractButtonId, (info) => {
if (info.privilege < config.timerActionsPrivilege) { return }
@@ -96,6 +107,8 @@ export default class TimerWidget extends StaticComponent {
time: tm.utils.getVerboseTime(tm.timer.remainingRaceTime)
}
tm.sendMessage(tm.utils.strVar(config.set, strObject))
+ this.timeXml = this.constructTimeXml()
+ tm.sendManialink(this.timeXml)
})
}
@@ -113,9 +126,11 @@ export default class TimerWidget extends StaticComponent {
private startDynamicTimerInterval() {
clearInterval(this.dynamicTimerInterval)
this.dynamicTimerInterval = setInterval(() => {
- this.noButtonXml = this.constructXml(false)
- this.xmlWithButtons = this.constructXml(true)
- this.sendMultipleManialinks(this.display())
+ if (!this.isDisplayed) { return }
+ if (this.isPaused && tm.timer.isPaused) { return }
+ this.timeXml = this.constructTimeXml()
+ tm.sendManialink(this.timeXml)
+ this.isPaused = tm.timer.isPaused
}, 300)
}
@@ -125,9 +140,10 @@ export default class TimerWidget extends StaticComponent {
// if (tm.getGameMode() === 'Stunts' && !tm.timer.isDynamic) {
// return [this.hide()]
// }
- const arr = []
+ const arr: (string | { xml: string, login: string })[] = [this.timeXml]
for (const e of tm.players.list) {
- arr.push(this.displayToPlayer(e.login, e.privilege))
+ const ml = this.displayToPlayer(e.login, e.privilege)
+ if (ml !== undefined) { arr.push(ml) }
}
this.isPaused = tm.timer.isPaused
return arr
@@ -140,49 +156,42 @@ export default class TimerWidget extends StaticComponent {
// }
privilege ??= tm.players.get(login)?.privilege ?? 0
if (this.isOnRestart || !tm.timer.isDynamic || privilege < config.timerActionsPrivilege) {
- return { xml: this.noButtonXml, login }
+ return { xml: this.headerXmlNoButtons, login }
} else {
- return { xml: this.xmlWithButtons, login }
+ return { xml: this.headerXmlWithButtons, login }
}
}
+ hide(): string {
+ return ``
+ }
+
+ hideToPlayer(login: string): { xml: string, login: string } {
+ return { xml: ``, login }
+ }
+
protected onPositionChange(): void {
- this.noButtonXml = this.constructXml(false)
- this.xmlWithButtons = this.constructXml(true)
+ this.headerXmlNoButtons = this.constructHeaderXml(false)
+ this.headerXmlWithButtons = this.constructHeaderXml(true)
+ this.timeXml = this.constructTimeXml()
this.sendMultipleManialinks(this.display())
}
- private constructXml(isDynamic: boolean): string {
+ /**
+ * Constructs the header manialink (icon, title, buttons, background quad).
+ * Only rebuilt on state changes, not on every timer tick.
+ * @param isDynamic Whether to include dynamic timer buttons
+ * @returns Header manialink XML string
+ */
+ private constructHeaderXml(isDynamic: boolean): string {
const headerHeight: number = this.header.options.height
let headerXml = isDynamic ? this.getButtonsXml() :
this.header.constructXml(config.title, config.icon, this.side)
- let timeXml = ''
let bottomH = config.height - (headerHeight + config.margin)
const isStunts = tm.getGameMode() === 'Stunts'
if (!isDynamic && isStunts) {
bottomH = config.stuntsHeight
}
- if (tm.timer.isDynamic && !this.isOnRestart) {
- if (tm.timer.isPaused) {
- timeXml = centeredText(config.pausedText, config.width, bottomH,
- { specialFont: true, yOffset: -0.3, xOffset: 0.2 })
- } else {
- const time = Math.floor(tm.timer.remainingRaceTime / 1000)
- let timeColour = config.timeColours[0]
- if (time < config.colourChangeThresholds[1]) {
- timeColour = config.timeColours[2]
- } else if (time < config.colourChangeThresholds[0]) {
- timeColour = config.timeColours[1]
- }
- const hoursAmount = ~~(time / (60 * 60))
- const hours = hoursAmount === 0 ? '' : `${hoursAmount.toString()}:`
- const minutes = (~~(time / 60) % 60).toString().padStart(2, '0')
- const seconds = (time % 60).toString().padStart(2, '0')
- const timeStr = hoursAmount < 100 ? `${hours}${minutes}:${seconds}` : `${hoursAmount} hours`
- timeXml = centeredText('$' + timeColour + timeStr, config.width, bottomH,
- { specialFont: true, yOffset: config.textYOffset })
- }
- }
let stuntsMargin = (isStunts && isDynamic) ? config.stuntsDynamicMarginTop : 0
if (isStunts && !isDynamic) {
return `
@@ -203,6 +212,48 @@ export default class TimerWidget extends StaticComponent {
+
+
+ `
+ }
+
+ /**
+ * Constructs the time display manialink (just the countdown text).
+ * Uses a separate manialink ID so the header doesn't need to be re-sent on every tick.
+ * @returns Time manialink XML string
+ */
+ private constructTimeXml(): string {
+ if (!tm.timer.isDynamic || this.isOnRestart) {
+ return ``
+ }
+ const headerHeight: number = this.header.options.height
+ const bottomH: number = config.height - (headerHeight + config.margin)
+ const isStunts = tm.getGameMode() === 'Stunts'
+ const stuntsMargin = isStunts ? config.stuntsDynamicMarginTop : 0
+ let timeXml = ''
+ if (tm.timer.isPaused) {
+ timeXml = centeredText(config.pausedText, config.width, bottomH,
+ { specialFont: true, yOffset: -0.3, xOffset: 0.2 })
+ } else {
+ const time = Math.floor(tm.timer.remainingRaceTime / 1000)
+ let timeColour = config.timeColours[0]
+ if (time < config.colourChangeThresholds[1]) {
+ timeColour = config.timeColours[2]
+ } else if (time < config.colourChangeThresholds[0]) {
+ timeColour = config.timeColours[1]
+ }
+ const hoursAmount = ~~(time / (60 * 60))
+ const hours = hoursAmount === 0 ? '' : `${hoursAmount.toString()}:`
+ const minutes = (~~(time / 60) % 60).toString().padStart(2, '0')
+ const seconds = (time % 60).toString().padStart(2, '0')
+ const timeStr = hoursAmount < 100 ? `${hours}${minutes}:${seconds}` : `${hoursAmount} hours`
+ timeXml = centeredText('$' + timeColour + timeStr, config.width, bottomH,
+ { specialFont: true, yOffset: config.textYOffset })
+ }
+ return `
+
+
+
${timeXml}
@@ -241,4 +292,4 @@ export default class TimerWidget extends StaticComponent {
this.side, { rectangleWidth: headerRectWidth }) + buttonXml
}
-}
+}
\ No newline at end of file