Skip to content
Closed
Changes from all commits
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
137 changes: 94 additions & 43 deletions plugins/ui/static_components/race/TimerWidget.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
Expand All @@ -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) => {
Expand All @@ -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 }
Expand All @@ -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 }
Expand All @@ -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)
})
}

Expand All @@ -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)
}

Expand All @@ -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
Expand All @@ -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 `<manialink id="${this.id}"></manialink><manialink id="${this.timeWidgetId}"></manialink>`
}

hideToPlayer(login: string): { xml: string, login: string } {
return { xml: `<manialink id="${this.id}"></manialink><manialink id="${this.timeWidgetId}"></manialink>`, 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 `
Expand All @@ -203,6 +212,48 @@ export default class TimerWidget extends StaticComponent {
<frame posn="0 ${-headerHeight - config.margin} -40">
<quad posn="0 0 1" sizen="${config.width} ${bottomH}" action="${this.pauseButtonId}"/>
<quad posn="0 0 -45" sizen="${config.width} ${bottomH}" bgcolor="${config.background}"/>
</frame>
</frame>
</manialink>`
}

/**
* 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 `<manialink id="${this.timeWidgetId}"></manialink>`
}
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 `
<manialink id="${this.timeWidgetId}">
<frame posn="${this.positionX} ${this.positionY - stuntsMargin} -38">
<frame posn="0 ${-headerHeight - config.margin} -40">
${timeXml}
</frame>
</frame>
Expand Down Expand Up @@ -241,4 +292,4 @@ export default class TimerWidget extends StaticComponent {
this.side, { rectangleWidth: headerRectWidth }) + buttonXml
}

}
}
Loading