707 lines
19 KiB
Vue
707 lines
19 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch, onMounted, onUnmounted, nextTick } 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 PlaybackBar from '@/components/PlaybackBar.vue'
|
|
import MainMenu from '@/components/MainMenu.vue'
|
|
import PauseMenu from '@/components/PauseMenu.vue'
|
|
import AchievementToast from '@/components/AchievementToast.vue'
|
|
import AchievementPanel from '@/components/AchievementPanel.vue'
|
|
import AccessibilitySettings from '@/components/AccessibilitySettings.vue'
|
|
import StoryGallery from '@/components/StoryGallery.vue'
|
|
import BattleHUD from '@/components/BattleHUD.vue'
|
|
import BattleResult from '@/components/BattleResult.vue'
|
|
import { useGameEngine } from '@/composables/useGameEngine'
|
|
import { useGameStore } from '@/stores/gameStore'
|
|
import { useFullscreen } from '@/composables/useFullscreen'
|
|
import { useI18n, initStoryLocales } from '@/composables/useI18n'
|
|
import { getVideoMode } from '@engine/core/VideoManager'
|
|
|
|
const isLocalMode = getVideoMode() === 'local'
|
|
|
|
const store = useGameStore()
|
|
const { t, currentLang } = useI18n()
|
|
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 showPauseMenu = ref(false)
|
|
const showStoryGallery = ref(false)
|
|
const showAchievements = ref(false)
|
|
const hasAutoSave = ref(false)
|
|
const currentSpeed = ref(1)
|
|
const canSkip = ref(false)
|
|
const paused = ref(false)
|
|
const promptToast = ref('')
|
|
const showPromptToast = ref(false)
|
|
const showIntro = ref(false)
|
|
const introWatched = ref(false)
|
|
const introStarted = ref(false)
|
|
const introVideoRef = ref<HTMLVideoElement | null>(null)
|
|
const menuVideoRef = ref<HTMLVideoElement | null>(null)
|
|
const showTopBar = ref(true)
|
|
let hideTopBarTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
const { loadGame, start, resumeAutoSave, makeChoice, clickHotspot, startChapter,
|
|
skipScene, setSpeed, getSpeed, isSceneWatched, startAtScene,
|
|
saveGame, loadGameFromSlot, refreshSaves, saveSystem, engine } =
|
|
useGameEngine(() => [videoElA.value, videoElB.value])
|
|
|
|
async function resolveScenePath(): Promise<string> {
|
|
const params = new URLSearchParams(location.search)
|
|
const queryScene = params.get('scene')
|
|
if (queryScene) return queryScene.startsWith('/') ? queryScene : '/' + queryScene
|
|
|
|
try {
|
|
const resp = await fetch('/scenes/config.json')
|
|
if (resp.ok) {
|
|
const config = await resp.json()
|
|
if (config.sceneFile) return '/scenes/' + config.sceneFile
|
|
}
|
|
} catch { /* config.json optional */ }
|
|
|
|
return '/scenes/main.json'
|
|
}
|
|
|
|
async function init() {
|
|
const scenePath = await resolveScenePath()
|
|
await loadGame(scenePath)
|
|
const lc = store.storyLocales
|
|
if (lc.path) {
|
|
await initStoryLocales(lc.path)
|
|
}
|
|
loading.value = false
|
|
hasAutoSave.value = (await saveSystem.load(0)) !== null
|
|
|
|
if (store.introVideo) {
|
|
introWatched.value = await isSceneWatched('__intro__')
|
|
const params = new URLSearchParams(location.search)
|
|
if (params.get('startScene')) {
|
|
showIntro.value = false
|
|
await nextTick()
|
|
handleStartFromScene(params.get('startScene')!)
|
|
return
|
|
}
|
|
showIntro.value = true
|
|
} else {
|
|
const params = new URLSearchParams(location.search)
|
|
const startSceneId = params.get('startScene')
|
|
if (startSceneId) {
|
|
await nextTick()
|
|
handleStartFromScene(startSceneId)
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleStartFromScene(sceneId: string) {
|
|
started.value = true
|
|
applyQteParams()
|
|
const ch = engine.sceneManager.chapters.find(c => {
|
|
const reachable = new Set<string>()
|
|
const queue = [c.startScene]
|
|
while (queue.length > 0) {
|
|
const id = queue.shift()!
|
|
if (reachable.has(id)) continue
|
|
const sc = engine.sceneManager.getScene(id)
|
|
if (!sc) continue
|
|
reachable.add(id)
|
|
if (sc.choices) for (const ch2 of sc.choices) if (ch2.targetScene) queue.push(ch2.targetScene)
|
|
if (sc.nextScene) {
|
|
if (Array.isArray(sc.nextScene)) for (const r of sc.nextScene) if (r.targetScene) queue.push(r.targetScene)
|
|
else queue.push(sc.nextScene)
|
|
}
|
|
if (sc.qte) { if (sc.qte.successScene) queue.push(sc.qte.successScene); if (sc.qte.failScene) queue.push(sc.qte.failScene) }
|
|
if (sc.hotspots) for (const h of sc.hotspots) if (h.targetScene) queue.push(h.targetScene)
|
|
}
|
|
return reachable.has(sceneId)
|
|
})
|
|
if (ch) {
|
|
startAtScene(ch.id, sceneId)
|
|
} else {
|
|
const scene = engine.sceneManager.getScene(sceneId)
|
|
if (scene) engine.goToScene(scene)
|
|
}
|
|
}
|
|
|
|
function onIntroEnded() {
|
|
saveSystem.markWatched('__intro__')
|
|
showIntro.value = false
|
|
}
|
|
|
|
function onIntroClick() {
|
|
if (!introStarted.value) {
|
|
introStarted.value = true
|
|
introVideoRef.value?.play().catch(() => {})
|
|
} else if (introWatched.value) {
|
|
skipIntro()
|
|
}
|
|
}
|
|
|
|
function skipIntro() {
|
|
saveSystem.markWatched('__intro__')
|
|
showIntro.value = false
|
|
}
|
|
|
|
function handleStart() {
|
|
started.value = true
|
|
store.setGameEnded(false)
|
|
applyQteParams()
|
|
start()
|
|
}
|
|
|
|
async function handleResume() {
|
|
started.value = true
|
|
applyQteParams()
|
|
await resumeAutoSave()
|
|
}
|
|
|
|
function applyQteParams() {
|
|
engine.qteSystem.timeLimitMultiplier = store.qteTimeRelax ? 1.5 : 1
|
|
engine.qteSystem.singleKeyMode = store.qteSingleKey
|
|
}
|
|
|
|
watch([() => store.qteTimeRelax, () => store.qteSingleKey], () => {
|
|
applyQteParams()
|
|
})
|
|
|
|
watch(() => store.preferredQuality, (q) => {
|
|
engine.videoManager.streamingQuality = q
|
|
const scene = store.currentScene
|
|
if (scene?.streamingUrl && scene.streamingUrl[q]) {
|
|
const active = engine.videoManager.getActiveVideoElement()
|
|
if (active && !active.ended) {
|
|
engine.videoManager.switchQuality(
|
|
engine.videoManager.resolveVideoUrl(scene, q),
|
|
engine.videoManager.getCurrentTime()
|
|
)
|
|
}
|
|
}
|
|
}, { immediate: true })
|
|
|
|
watch(
|
|
() => [
|
|
showPauseMenu.value, showMenu.value,
|
|
showAchievements.value, showStoryGallery.value,
|
|
paused.value, store.showSettings,
|
|
],
|
|
() => { resetTopBarTimer() },
|
|
)
|
|
|
|
function onVideoReady(elA: HTMLVideoElement, elB: HTMLVideoElement) {
|
|
videoElA.value = elA
|
|
videoElB.value = elB
|
|
}
|
|
|
|
function onChoose(index: number) {
|
|
makeChoice(index)
|
|
}
|
|
|
|
function onPrompt(text: string) {
|
|
promptToast.value = text
|
|
showPromptToast.value = true
|
|
setTimeout(() => { showPromptToast.value = false }, 2500)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
async function onStartChapter(chapterId: string) {
|
|
started.value = true
|
|
applyQteParams()
|
|
startChapter(chapterId)
|
|
}
|
|
|
|
function handleSkip() {
|
|
skipScene()
|
|
}
|
|
|
|
function handleSpeedChange(rate: number) {
|
|
setSpeed(rate)
|
|
currentSpeed.value = rate
|
|
}
|
|
|
|
function onBattleResultContinue() {
|
|
store.setShowBattleResult(false)
|
|
const scene = store.currentScene
|
|
if (!scene) return
|
|
if (scene.nextScene) {
|
|
if (Array.isArray(scene.nextScene)) {
|
|
for (const route of scene.nextScene) {
|
|
if (!route.conditions || engine.stateManager.evaluate(route.conditions)) {
|
|
const next = engine.sceneManager.getScene(route.targetScene)
|
|
if (next) { engine.goToScene(next) }
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
const next = engine.sceneManager.getScene(scene.nextScene)
|
|
if (next) { engine.goToScene(next) }
|
|
return
|
|
}
|
|
}
|
|
if (scene.choices && scene.choices.length > 0) {
|
|
const first = scene.choices[0]
|
|
const next = engine.sceneManager.getScene(first.targetScene)
|
|
if (next) { engine.goToScene(next) }
|
|
}
|
|
}
|
|
|
|
watch(() => store.currentScene?.id, async (newId) => {
|
|
if (!newId) { canSkip.value = false; return }
|
|
const scene = store.currentScene
|
|
if (scene?.skippable === false) { canSkip.value = false; return }
|
|
canSkip.value = await isSceneWatched(newId)
|
|
})
|
|
|
|
function onGlobalKeydown(e: KeyboardEvent) {
|
|
const key = e.key
|
|
|
|
if (key === 'p' && !store.qteActive && store.pauseEnabled && started.value && !store.gameEnded) {
|
|
const activeEl = document.activeElement
|
|
if (!activeEl || activeEl.tagName === 'BODY' || activeEl === document.body) {
|
|
e.preventDefault()
|
|
togglePause()
|
|
return
|
|
}
|
|
}
|
|
|
|
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'ArrowLeft' || key === 'ArrowRight' ||
|
|
key === 'Enter' || key === ' ' || key === 'Tab' || key === 'w' || key === 'a' || key === 's' || key === 'd') {
|
|
store.setInputMode('keyboard')
|
|
}
|
|
if (key === 'Escape') {
|
|
if (store.showSettings) {
|
|
store.showSettings = false
|
|
} else if (showStoryGallery.value) {
|
|
showStoryGallery.value = false
|
|
} else if (showMenu.value) {
|
|
showMenu.value = false
|
|
} else if (showPauseMenu.value) {
|
|
showPauseMenu.value = false
|
|
} else if (showAchievements.value) {
|
|
showAchievements.value = false
|
|
} else if (started.value && !store.gameEnded) {
|
|
showPauseMenu.value = true
|
|
}
|
|
}
|
|
}
|
|
|
|
function togglePause() {
|
|
if (!started.value || store.gameEnded) return
|
|
if (paused.value) {
|
|
paused.value = false
|
|
engine.videoManager.getActiveVideoElement()?.play().catch(() => {})
|
|
} else {
|
|
paused.value = true
|
|
engine.videoManager.getActiveVideoElement()?.pause()
|
|
}
|
|
}
|
|
|
|
function onPauseResume() {
|
|
showPauseMenu.value = false
|
|
}
|
|
|
|
function onPauseSaveLoad() {
|
|
showPauseMenu.value = false
|
|
showMenu.value = true
|
|
refreshSaves()
|
|
}
|
|
|
|
function onPauseSettings() {
|
|
showPauseMenu.value = false
|
|
store.setShowSettings(true)
|
|
}
|
|
|
|
function onQuitToMenu() {
|
|
showPauseMenu.value = false
|
|
store.setGameEnded(true)
|
|
engine.qteSystem.cancel()
|
|
engine.audioSystem.stop(2.0)
|
|
}
|
|
|
|
function onGlobalMouseMove() {
|
|
store.setInputMode('mouse')
|
|
resetTopBarTimer()
|
|
}
|
|
|
|
function anyOverlayOpen(): boolean {
|
|
return showPauseMenu.value || showMenu.value || showStoryGallery.value
|
|
|| showAchievements.value || paused.value || store.showSettings
|
|
}
|
|
|
|
function resetTopBarTimer() {
|
|
showTopBar.value = true
|
|
if (hideTopBarTimer) clearTimeout(hideTopBarTimer)
|
|
if (!anyOverlayOpen()) {
|
|
hideTopBarTimer = setTimeout(() => { showTopBar.value = false }, 3000)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('keydown', onGlobalKeydown)
|
|
document.addEventListener('mousemove', onGlobalMouseMove)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('keydown', onGlobalKeydown)
|
|
document.removeEventListener('mousemove', onGlobalMouseMove)
|
|
if (hideTopBarTimer) clearTimeout(hideTopBarTimer)
|
|
})
|
|
|
|
init()
|
|
</script>
|
|
|
|
<template>
|
|
<div class="app-container">
|
|
<div v-if="loading" class="loading">{{ t('ui.loading') }}</div>
|
|
<template v-else>
|
|
<div v-if="showIntro" class="intro-overlay" @click="onIntroClick">
|
|
<video ref="introVideoRef" :src="store.introVideo" class="intro-video" @ended="onIntroEnded"></video>
|
|
<div v-if="!introStarted" class="intro-start-prompt">点击开始</div>
|
|
<button v-if="introWatched && introStarted" class="intro-skip-btn" @click.stop="skipIntro">{{ t('ui.skip') }}</button>
|
|
</div>
|
|
|
|
<div v-if="store.menuVideo && (!started || store.gameEnded)" class="menu-bg">
|
|
<video ref="menuVideoRef" :src="store.menuVideo" class="menu-bg-video" autoplay loop muted></video>
|
|
</div>
|
|
|
|
<div class="game-screen" :class="{ idle: !showTopBar }" v-show="started && !store.gameEnded">
|
|
<GamePlayer v-show="!store.isImageScene" @video-ready="onVideoReady" />
|
|
<HotspotLayer
|
|
:hotspots="store.hotspots"
|
|
:is-image-scene="store.isImageScene"
|
|
:image-url="store.currentScene?.imageUrl"
|
|
:content-size="store.currentScene?.contentSize ?? null"
|
|
@click-hotspot="clickHotspot"
|
|
/>
|
|
<Subtitles
|
|
:current-time="store.videoTime"
|
|
:subtitle-url="store.currentScene?.subtitleUrl ?? null"
|
|
:subtitles="store.currentScene?.subtitles ?? null"
|
|
:lang="currentLang"
|
|
/>
|
|
<QTEOverlay
|
|
:visible="store.qteActive"
|
|
:prompt="store.qteDef ? t(store.qteDef.promptKey || 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"
|
|
@prompt="onPrompt"
|
|
/>
|
|
<Transition name="prompt-toast">
|
|
<div v-if="showPromptToast" class="prompt-toast">{{ promptToast }}</div>
|
|
</Transition>
|
|
<BattleHUD
|
|
v-if="store.currentScene?.battleHUD"
|
|
:entries="store.currentScene!.battleHUD"
|
|
/>
|
|
<div v-if="started && !store.gameEnded && store.choices.length === 0" class="top-bar" :class="{ hidden: !showTopBar }">
|
|
<button class="top-btn" @click="toggleFullscreen" :title="t('ui.fullscreen')">⛶</button>
|
|
<button class="top-btn" @click="showPauseMenu = true" :title="t('ui.menu')">≡</button>
|
|
</div>
|
|
</div>
|
|
<PlaybackBar
|
|
v-if="started && !store.gameEnded"
|
|
:can-skip="canSkip"
|
|
:current-speed="currentSpeed"
|
|
:visible="showTopBar"
|
|
:show-quality="!isLocalMode"
|
|
:hide="store.choices.length > 0"
|
|
@skip="handleSkip"
|
|
@speed-change="handleSpeedChange"
|
|
/>
|
|
<Transition name="battle-result">
|
|
<BattleResult
|
|
v-if="store.showBattleResult"
|
|
:result="store.battleResultData"
|
|
@continue="onBattleResultContinue"
|
|
/>
|
|
</Transition>
|
|
<MainMenu
|
|
v-if="!started || store.gameEnded"
|
|
:show-resume="!store.gameEnded && hasAutoSave"
|
|
:show-story="store.chapters.length > 0"
|
|
:show-achievements="store.achievementDefs.length > 0"
|
|
:is-game-end="store.gameEnded"
|
|
@start="handleStart"
|
|
@resume="handleResume"
|
|
@story="showStoryGallery = true"
|
|
@achievements="showAchievements = true"
|
|
@settings="store.setShowSettings(true)"
|
|
/>
|
|
<PauseMenu
|
|
v-if="showPauseMenu"
|
|
@resume="onPauseResume"
|
|
@save-load="onPauseSaveLoad"
|
|
@settings="onPauseSettings"
|
|
@quit-to-menu="onQuitToMenu"
|
|
/>
|
|
<SaveLoadMenu
|
|
v-if="showMenu"
|
|
:saves="store.saves"
|
|
@save="onSave"
|
|
@load="onLoad"
|
|
@close="showMenu = false"
|
|
/>
|
|
<StoryGallery
|
|
v-if="showStoryGallery"
|
|
:chapters="store.chapters"
|
|
:endings="store.endings"
|
|
:scenes="engine.sceneManager.getScenes()"
|
|
:visited-ids="store.visitedSceneIds"
|
|
:unlocked-chapter-ids="store.unlockedChapterIds"
|
|
@start-chapter="(chId: string) => { showStoryGallery = false; onStartChapter(chId) }"
|
|
@start-at-scene="(chId: string, sceneId: string) => { showStoryGallery = false; started = true; startAtScene(chId, sceneId) }"
|
|
@close="showStoryGallery = false"
|
|
/>
|
|
<AchievementPanel
|
|
v-if="showAchievements"
|
|
:definitions="store.achievementDefs"
|
|
:unlocked-ids="store.unlockedAchievementIds"
|
|
@close="showAchievements = false"
|
|
/>
|
|
<AccessibilitySettings
|
|
v-if="store.showSettings"
|
|
@close="store.setShowSettings(false)"
|
|
/>
|
|
<div v-if="paused" class="pause-overlay" @click="togglePause">
|
|
<div class="pause-text">已暂停</div>
|
|
<div class="pause-hint">点击或按 P 继续</div>
|
|
</div>
|
|
<AchievementToast
|
|
:achievement-id="store.toastAchievementId"
|
|
:definitions="store.achievementDefs"
|
|
@done="store.clearToastAchievement()"
|
|
/>
|
|
</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;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.top-bar.hidden {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.game-screen.idle {
|
|
cursor: none;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.pause-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.75);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 150;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.pause-text {
|
|
font-size: 36px;
|
|
color: #fff;
|
|
letter-spacing: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.pause-hint {
|
|
font-size: 14px;
|
|
color: #888;
|
|
}
|
|
|
|
.intro-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: #000;
|
|
z-index: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.intro-video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.intro-skip-btn {
|
|
position: absolute;
|
|
bottom: 40px;
|
|
right: 40px;
|
|
padding: 10px 24px;
|
|
font-size: 14px;
|
|
color: #fff;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
letter-spacing: 2px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.intro-skip-btn:hover {
|
|
background: rgba(0, 0, 0, 0.7);
|
|
}
|
|
|
|
.intro-start-prompt {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
padding: 18px 48px;
|
|
font-size: 22px;
|
|
color: #fff;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 6px;
|
|
letter-spacing: 4px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.intro-start-prompt:hover {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.menu-bg {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 0;
|
|
}
|
|
|
|
.menu-bg-video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.prompt-toast {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
padding: 16px 36px;
|
|
font-size: 20px;
|
|
color: #ffc107;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border: 1px solid rgba(255, 193, 7, 0.4);
|
|
border-radius: 6px;
|
|
letter-spacing: 3px;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
z-index: 50;
|
|
}
|
|
|
|
.prompt-toast-enter-active { transition: opacity 0.3s ease; }
|
|
.prompt-toast-leave-active { transition: opacity 0.8s ease; }
|
|
.prompt-toast-enter-from,
|
|
.prompt-toast-leave-to { opacity: 0; }
|
|
</style>
|
|
|
|
<style>
|
|
.battle-result-enter-active { transition: all 0.3s ease-out; }
|
|
.battle-result-leave-active { transition: all 0.2s ease-in; }
|
|
.battle-result-enter-from { opacity: 0; }
|
|
.battle-result-leave-to { opacity: 0; transform: scale(0.95); }
|
|
</style>
|