feat: P1 core - seamless video switching, conditional branches, save/load

- VideoManager: A/B dual-buffered video with crossfade transitions and candidate preloading
- Engine: condition-based choice filtering, ChoiceSystem timer, resumeScene for save/load
- SceneManager: getCandidateUrls for preloading next scenes
- SaveSystem: Dexie.js IndexedDB multi-slot save/load
- ChoiceSystem: timed choices with countdown and auto-default on timeout
- GamePlayer: dual video elements with crossfade CSS
- ChoicePanel: timer progress bar and countdown text
- SaveLoadMenu: save/load UI component
- App.vue: menu trigger, dual video refs, save/load integration
- gameStore: timer state, saves list
- demo.json: conditional choice example (secret ending, requires trust >= 80)
- ROADMAP: mark P1 as completed
This commit is contained in:
2026-06-07 16:48:52 +08:00
parent 42181fe185
commit 937e45c203
16 changed files with 763 additions and 71 deletions

View File

@@ -3,15 +3,35 @@ import type { Choice } from '@engine/types'
const props = defineProps<{
choices: Choice[]
timerTotal: number
timerRemaining: number
}>()
const emit = defineEmits<{
choose: [index: number]
}>()
function timerPercent(): number {
if (props.timerTotal <= 0) return 0
return (props.timerRemaining / props.timerTotal) * 100
}
function timerClass(): string {
if (props.timerRemaining <= 3) return 'danger'
return ''
}
</script>
<template>
<div class="choice-panel" v-if="choices.length > 0">
<div class="timer-bar" v-if="timerTotal > 0">
<div
class="timer-fill"
:class="timerClass()"
:style="{ width: timerPercent() + '%' }"
></div>
<span class="timer-text">{{ timerRemaining.toFixed(1) }}s</span>
</div>
<div class="choice-prompt">做出你的选择</div>
<div class="choice-list">
<button
@@ -33,10 +53,38 @@ const emit = defineEmits<{
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.85));
padding: 40px 20px 30px;
padding: 20px 20px 30px;
z-index: 10;
}
.timer-bar {
height: 4px;
background: rgba(255, 255, 255, 0.15);
border-radius: 2px;
margin-bottom: 16px;
position: relative;
overflow: hidden;
}
.timer-fill {
height: 100%;
background: rgba(255, 255, 255, 0.6);
transition: width 0.1s linear;
border-radius: 2px;
}
.timer-fill.danger {
background: #e74c3c;
}
.timer-text {
position: absolute;
top: -18px;
right: 0;
font-size: 12px;
color: #aaa;
}
.choice-prompt {
color: #ccc;
font-size: 14px;