Files
tianshu-engine/engine/systems/QTESystem.ts
cocos02 b6eb3c3959 fix: QTE false race condition + restore auto-save slot in menu
- QTESystem: add if (!this.active) return guard in setInterval/setTimeout
  callbacks to prevent false result from firing after successful key press
- SaveLoadMenu: restore auto-save slot 0 row with blue styling, thumbnail,
  scene label, and read-only load button
2026-06-07 21:07:21 +08:00

81 lines
2.1 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(() => {
if (!this.active) return
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(() => {
if (!this.active) return
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()
}
}