feat: engine improvements, new scenes, videos, subtitles, hotspot component and docs update
This commit is contained in:
11
src/App.vue
11
src/App.vue
@@ -4,6 +4,7 @@ import GamePlayer from '@/components/GamePlayer.vue'
|
||||
import ChoicePanel from '@/components/ChoicePanel.vue'
|
||||
import QTEOverlay from '@/components/QTEOverlay.vue'
|
||||
import Subtitles from '@/components/Subtitles.vue'
|
||||
import HotspotLayer from '@/components/HotspotLayer.vue'
|
||||
import SaveLoadMenu from '@/components/SaveLoadMenu.vue'
|
||||
import { useGameEngine } from '@/composables/useGameEngine'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
@@ -16,7 +17,7 @@ const started = ref(false)
|
||||
const showMenu = ref(false)
|
||||
const hasAutoSave = ref(false)
|
||||
|
||||
const { loadGame, start, resumeAutoSave, makeChoice, saveGame, loadGameFromSlot, refreshSaves, saveSystem } =
|
||||
const { loadGame, start, resumeAutoSave, makeChoice, clickHotspot, saveGame, loadGameFromSlot, refreshSaves, saveSystem } =
|
||||
useGameEngine(() => [videoElA.value, videoElB.value])
|
||||
|
||||
async function init() {
|
||||
@@ -68,7 +69,13 @@ init()
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<template v-else>
|
||||
<div class="game-screen">
|
||||
<GamePlayer @video-ready="onVideoReady" />
|
||||
<GamePlayer v-if="!store.isImageScene" @video-ready="onVideoReady" />
|
||||
<HotspotLayer
|
||||
:hotspots="store.hotspots"
|
||||
:is-image-scene="store.isImageScene"
|
||||
:image-url="store.currentScene?.imageUrl"
|
||||
@click-hotspot="clickHotspot"
|
||||
/>
|
||||
<Subtitles
|
||||
:current-time="store.videoTime"
|
||||
:subtitle-url="store.currentScene?.subtitleUrl ?? null"
|
||||
|
||||
85
src/components/HotspotLayer.vue
Normal file
85
src/components/HotspotLayer.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import type { Hotspot } from '@engine/types'
|
||||
|
||||
const props = defineProps<{
|
||||
hotspots: Hotspot[]
|
||||
isImageScene: boolean
|
||||
imageUrl?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
clickHotspot: [hotspotId: string]
|
||||
}>()
|
||||
|
||||
function hsStyle(hs: Hotspot) {
|
||||
return {
|
||||
left: `${hs.x * 100}%`,
|
||||
top: `${hs.y * 100}%`,
|
||||
width: `${hs.width * 100}%`,
|
||||
height: `${hs.height * 100}%`,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hotspot-layer" v-if="(hotspots.length > 0) || (isImageScene && imageUrl)">
|
||||
<img v-if="isImageScene && imageUrl" :src="imageUrl" class="hotspot-image" />
|
||||
|
||||
<div
|
||||
v-for="hs in hotspots"
|
||||
:key="hs.id"
|
||||
class="hotspot-rect"
|
||||
:style="hsStyle(hs)"
|
||||
@click.stop="emit('clickHotspot', hs.id)"
|
||||
:title="hs.label"
|
||||
>
|
||||
<span class="hotspot-label">{{ hs.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hotspot-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hotspot-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.hotspot-rect {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hotspot-rect:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.hotspot-label {
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
padding: 3px 10px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,8 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide
|
||||
store.setScene(scene)
|
||||
store.clearChoices()
|
||||
store.clearTimer()
|
||||
store.clearHotspots()
|
||||
store.setIsImageScene(scene.type === 'image')
|
||||
saveGame(0)
|
||||
})
|
||||
|
||||
@@ -43,6 +45,14 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide
|
||||
store.clearTimer()
|
||||
})
|
||||
|
||||
engine.on('hotspotRequest', (list) => {
|
||||
store.setHotspots(list)
|
||||
})
|
||||
|
||||
engine.on('hotspotUpdate', (list) => {
|
||||
store.setHotspots(list)
|
||||
})
|
||||
|
||||
engine.on('videoEnd', () => {
|
||||
try {
|
||||
const video = engine.videoManager.getActiveVideoElement()
|
||||
@@ -83,14 +93,14 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide
|
||||
|
||||
function start() {
|
||||
const [elA, elB] = videoEls()
|
||||
engine.videoManager.attach(elA!, elB!)
|
||||
if (elA && elB) engine.videoManager.attach(elA, elB)
|
||||
registerEvents()
|
||||
engine.start()
|
||||
}
|
||||
|
||||
async function resumeAutoSave(): Promise<boolean> {
|
||||
const [elA, elB] = videoEls()
|
||||
engine.videoManager.attach(elA!, elB!)
|
||||
if (elA && elB) engine.videoManager.attach(elA, elB)
|
||||
registerEvents()
|
||||
return await loadGameFromSlot(0)
|
||||
}
|
||||
@@ -103,6 +113,15 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide
|
||||
engine.makeChoice(scene.choices[index])
|
||||
}
|
||||
|
||||
function clickHotspot(hotspotId: string) {
|
||||
const scene = store.currentScene
|
||||
if (!scene?.hotspots) return
|
||||
const hs = scene.hotspots.find((h) => h.id === hotspotId)
|
||||
if (hs) {
|
||||
engine.clickHotspot(hs)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGame(slot: number) {
|
||||
const state = engine.stateManager
|
||||
const currentScene = store.currentScene
|
||||
@@ -144,5 +163,5 @@ export function useGameEngine(videoEls: () => [HTMLVideoElement | null, HTMLVide
|
||||
destroy()
|
||||
})
|
||||
|
||||
return { loadGame, start, resumeAutoSave, makeChoice, saveGame, loadGameFromSlot, refreshSaves, engine, saveSystem }
|
||||
return { loadGame, start, resumeAutoSave, makeChoice, clickHotspot, saveGame, loadGameFromSlot, refreshSaves, engine, saveSystem }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import type { SceneNode, Choice, QTEDefinition } from '@engine/types'
|
||||
import type { SceneNode, Choice, QTEDefinition, Hotspot } from '@engine/types'
|
||||
|
||||
export interface SlotInfo {
|
||||
slot: number
|
||||
@@ -23,6 +23,8 @@ export const useGameStore = defineStore('game', () => {
|
||||
const qteRemaining = ref(0)
|
||||
const qteResult = ref<'none' | 'success' | 'fail'>('none')
|
||||
const videoTime = ref(0)
|
||||
const hotspots = ref<Hotspot[]>([])
|
||||
const isImageScene = ref(false)
|
||||
|
||||
function setScene(scene: SceneNode) {
|
||||
currentScene.value = scene
|
||||
@@ -79,6 +81,18 @@ export const useGameStore = defineStore('game', () => {
|
||||
videoTime.value = t
|
||||
}
|
||||
|
||||
function setHotspots(list: Hotspot[]) {
|
||||
hotspots.value = list
|
||||
}
|
||||
|
||||
function clearHotspots() {
|
||||
hotspots.value = []
|
||||
}
|
||||
|
||||
function setIsImageScene(val: boolean) {
|
||||
isImageScene.value = val
|
||||
}
|
||||
|
||||
function dump() {
|
||||
console.group('GameStore')
|
||||
console.log('currentScene:', currentScene.value?.id)
|
||||
@@ -94,9 +108,11 @@ export const useGameStore = defineStore('game', () => {
|
||||
return {
|
||||
currentScene, choices, gameEnded, timerTotal, timerRemaining, saves,
|
||||
qteActive, qteDef, qteTotal, qteRemaining, qteResult, videoTime,
|
||||
hotspots, isImageScene,
|
||||
setScene, setChoices, clearChoices, setGameEnded,
|
||||
setTimer, clearTimer, setSaves,
|
||||
showQTE, updateQTE, resolveQTE, setVideoTime,
|
||||
setHotspots, clearHotspots, setIsImageScene,
|
||||
dump,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user