feat: P2 - QTE system, subtitles, save thumbnails

- QTESystem: trigger detection via timeupdate, multi-key matching, timeout handling
- QTEOverlay: SVG countdown ring + key prompts + success/fail animation
- Engine: integrate QTE (timeupdate check, conditional branching, effect application)
- Subtitles: WebVTT parsing + synchronized subtitle rendering
- GamePlayer: overlay QTE and subtitle components
- SaveSystem: DB v2 with thumbnail field, canvas snapshot at 320x180 JPEG
- SaveLoadMenu: thumbnail preview for save slots
- VideoManager: getActiveVideoElement() for canvas capture
- App.vue: QTE/subtitle integration, thumbnail capture on save
- stores: QTE state management, save list with thumbnails
- demo.json: QTE scene (right_door), subtitles, new event types
- ROADMAP: mark P2 as completed
This commit is contained in:
2026-06-07 19:35:14 +08:00
parent c168e30e52
commit 319a379921
18 changed files with 625 additions and 53 deletions

View File

@@ -1,11 +1,12 @@
import { defineStore } from 'pinia'
import { ref, shallowRef } from 'vue'
import type { SceneNode, Choice } from '@engine/types'
import type { SceneNode, Choice, QTEDefinition } from '@engine/types'
export interface SlotInfo {
slot: number
timestamp: number
sceneLabel: string
thumbnail?: string
}
export const useGameStore = defineStore('game', () => {
@@ -16,6 +17,13 @@ export const useGameStore = defineStore('game', () => {
const timerRemaining = ref(0)
const saves = ref<SlotInfo[]>([])
const qteActive = ref(false)
const qteDef = shallowRef<QTEDefinition | null>(null)
const qteTotal = ref(0)
const qteRemaining = ref(0)
const qteResult = ref<'none' | 'success' | 'fail'>('none')
const videoTime = ref(0)
function setScene(scene: SceneNode) {
currentScene.value = scene
}
@@ -46,9 +54,36 @@ export const useGameStore = defineStore('game', () => {
saves.value = list
}
function showQTE(qte: QTEDefinition) {
qteActive.value = true
qteDef.value = qte
qteTotal.value = qte.timeLimit
qteRemaining.value = qte.timeLimit
qteResult.value = 'none'
}
function updateQTE(remaining: number) {
qteRemaining.value = remaining
}
function resolveQTE(success: boolean) {
qteResult.value = success ? 'success' : 'fail'
setTimeout(() => {
qteActive.value = false
qteDef.value = null
qteResult.value = 'none'
}, 1000)
}
function setVideoTime(t: number) {
videoTime.value = t
}
return {
currentScene, choices, gameEnded, timerTotal, timerRemaining, saves,
qteActive, qteDef, qteTotal, qteRemaining, qteResult, videoTime,
setScene, setChoices, clearChoices, setGameEnded,
setTimer, clearTimer, setSaves,
showQTE, updateQTE, resolveQTE, setVideoTime,
}
})