79 lines
2.0 KiB
TypeScript
79 lines
2.0 KiB
TypeScript
import type { QTEDefinition } from '../types'
|
|
|
|
type QTEUpdateCallback = (remaining: number, total: number) => void
|
|
type QTEResultCallback = (success: boolean) => void
|
|
|
|
export class QTESystem {
|
|
// QTE (Quick Time Event / 快速反应事件):
|
|
// 视频播放到特定时间点时弹出按键提示,玩家在倒计时内按下指定按键,
|
|
// 成功/失败/超时分别导向不同剧情分支,并应用对应的 effects 效果。
|
|
private timerId: ReturnType<typeof setInterval> | null = null
|
|
private timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
private keyHandler: ((e: KeyboardEvent) => void) | null = null
|
|
private tickMs = 50
|
|
private active = false
|
|
|
|
trigger(
|
|
qte: QTEDefinition,
|
|
onUpdate: QTEUpdateCallback,
|
|
onResult: QTEResultCallback,
|
|
) {
|
|
if (this.active) return
|
|
this.active = true
|
|
|
|
const startTime = Date.now()
|
|
const total = qte.timeLimit * 1000
|
|
|
|
this.keyHandler = (e: KeyboardEvent) => {
|
|
if (!this.active) return
|
|
const matched = qte.keys.some(
|
|
(k) => k.toLowerCase() === e.key.toLowerCase()
|
|
)
|
|
if (matched) {
|
|
this.clear()
|
|
onResult(true)
|
|
}
|
|
}
|
|
document.addEventListener('keydown', this.keyHandler)
|
|
|
|
this.timerId = setInterval(() => {
|
|
const elapsed = Date.now() - startTime
|
|
const remaining = Math.max(0, total - elapsed)
|
|
onUpdate(remaining / 1000, qte.timeLimit)
|
|
if (remaining <= 0) {
|
|
this.clear()
|
|
onResult(false)
|
|
}
|
|
}, this.tickMs)
|
|
|
|
this.timeoutId = setTimeout(() => {
|
|
this.clear()
|
|
onResult(false)
|
|
}, total)
|
|
}
|
|
|
|
cancel() {
|
|
this.clear()
|
|
}
|
|
|
|
private clear() {
|
|
this.active = false
|
|
if (this.timerId !== null) {
|
|
clearInterval(this.timerId)
|
|
this.timerId = null
|
|
}
|
|
if (this.timeoutId !== null) {
|
|
clearTimeout(this.timeoutId)
|
|
this.timeoutId = null
|
|
}
|
|
if (this.keyHandler !== null) {
|
|
document.removeEventListener('keydown', this.keyHandler)
|
|
this.keyHandler = null
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.clear()
|
|
}
|
|
}
|