Files
tianshu-engine/src/App.vue

294 lines
6.8 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
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 ChapterSelect from '@/components/ChapterSelect.vue'
import { useGameEngine } from '@/composables/useGameEngine'
import { useGameStore } from '@/stores/gameStore'
import { useFullscreen } from '@/composables/useFullscreen'
const store = useGameStore()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
const videoElA = ref<HTMLVideoElement | null>(null)
const videoElB = ref<HTMLVideoElement | null>(null)
const loading = ref(true)
const started = ref(false)
const showMenu = ref(false)
const showChapterSelect = ref(false)
const hasAutoSave = ref(false)
const { loadGame, start, resumeAutoSave, makeChoice, clickHotspot, startChapter,
saveGame, loadGameFromSlot, refreshSaves, saveSystem } =
useGameEngine(() => [videoElA.value, videoElB.value])
async function init() {
await loadGame('/scenes/demo.json')
loading.value = false
hasAutoSave.value = (await saveSystem.load(0)) !== null
}
function handleStart() {
started.value = true
start()
}
async function handleResume() {
started.value = true
await resumeAutoSave()
}
function onVideoReady(elA: HTMLVideoElement, elB: HTMLVideoElement) {
videoElA.value = elA
videoElB.value = elB
}
function onChoose(index: number) {
makeChoice(index)
}
function toggleMenu() {
showMenu.value = !showMenu.value
if (showMenu.value) {
refreshSaves()
}
}
async function onSave(slot: number) {
await saveGame(slot)
}
async function onLoad(slot: number) {
await loadGameFromSlot(slot)
showMenu.value = false
}
function openChapterSelect() {
showMenu.value = false
showChapterSelect.value = true
}
async function onStartChapter(chapterId: string) {
showChapterSelect.value = false
started.value = true
startChapter(chapterId)
}
init()
</script>
<template>
<div class="app-container">
<div v-if="loading" class="loading">加载中...</div>
<template v-else>
<div class="game-screen">
<GamePlayer v-show="!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"
/>
<QTEOverlay
:visible="store.qteActive"
:prompt="store.qteDef?.prompt ?? ''"
:keys="store.qteDef?.keys ?? []"
:total="store.qteTotal"
:remaining="store.qteRemaining"
:result="store.qteResult"
/>
<ChoicePanel
v-if="!store.qteActive"
:choices="store.choices"
:timer-total="store.timerTotal"
:timer-remaining="store.timerRemaining"
@choose="onChoose"
/>
<div v-if="started && !store.gameEnded" class="top-bar">
<button class="top-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
{{ isFullscreen ? '⛶' : '⛶' }}
</button>
<button class="top-btn" @click="toggleMenu">菜单</button>
</div>
</div>
<div v-if="!started" class="start-overlay">
<button class="start-btn" @click="handleStart">开始游戏</button>
<button v-if="hasAutoSave" class="start-btn resume-btn" @click="handleResume">继续上次进度</button>
<button v-if="store.chapters.length > 0" class="start-btn chapters-btn" @click="openChapterSelect">章节选择</button>
</div>
<div v-if="store.gameEnded" class="game-end-overlay">
<div class="game-end-text">游戏结束</div>
<div v-if="store.chapters.length > 0" class="game-end-actions">
<button class="end-btn" @click="openChapterSelect">章节选择</button>
</div>
</div>
<SaveLoadMenu
v-if="showMenu"
:saves="store.saves"
@save="onSave"
@load="onLoad"
@close="showMenu = false"
/>
<ChapterSelect
v-if="showChapterSelect"
:chapters="store.chapters"
:unlocked-ids="store.unlockedChapterIds"
@select="onStartChapter"
@back="showChapterSelect = false"
/>
</template>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#app {
width: 100%;
height: 100%;
}
</style>
<style scoped>
.app-container {
width: 100%;
height: 100%;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 18px;
color: #888;
}
.game-screen {
position: relative;
width: 100%;
height: 100%;
}
.top-bar {
position: absolute;
top: 16px;
right: 16px;
z-index: 20;
display: flex;
gap: 6px;
}
.top-btn {
padding: 8px 16px;
font-size: 13px;
color: #aaa;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 3px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.top-btn:hover {
background: rgba(0, 0, 0, 0.7);
color: #fff;
}
.game-end-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.game-end-text {
font-size: 36px;
letter-spacing: 4px;
}
.start-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.start-btn {
padding: 18px 48px;
font-size: 20px;
color: #fff;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
cursor: pointer;
letter-spacing: 4px;
transition: background 0.2s;
}
.start-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.resume-btn {
margin-top: 16px;
border-color: rgba(100, 200, 255, 0.3);
color: #8cf;
}
.chapters-btn {
margin-top: 16px;
border-color: rgba(255, 200, 100, 0.3);
color: #fc8;
}
.game-end-actions {
margin-top: 24px;
display: flex;
gap: 12px;
justify-content: center;
}
.end-btn {
padding: 14px 32px;
font-size: 18px;
color: #fc8;
background: rgba(255, 200, 100, 0.08);
border: 1px solid rgba(255, 200, 100, 0.2);
border-radius: 4px;
cursor: pointer;
letter-spacing: 2px;
transition: background 0.2s;
}
.end-btn:hover {
background: rgba(255, 200, 100, 0.15);
}
</style>