Epic 4: Chemins Narratifs, Challenge & Contact Stories implementees: - 4.1: Composant ChoiceCards pour choix narratifs binaires - 4.2: Sequence d'intro narrative avec Le Bug - 4.3: Chemins narratifs differencies avec useNarrativePath - 4.4: Table easter_eggs et systeme de detection (API + composable) - 4.5: Easter eggs UI (popup, notification, collection) - 4.6: Page challenge avec puzzle de code - 4.7: Page revelation "Monde de Code" - 4.8: Page contact avec formulaire et stats Fichiers crees: - Frontend: ChoiceCards, IntroSequence, ZoneEndChoice, EasterEggPopup, CodePuzzle, ChallengeSuccess, CodeWorld, et pages intro/challenge/revelation - API: EasterEggController, Model, Migration, Seeder Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
155 lines
4.2 KiB
Vue
155 lines
4.2 KiB
Vue
<template>
|
|
<div class="intro-page min-h-screen bg-sky-dark relative overflow-hidden">
|
|
<!-- Fond d'ambiance -->
|
|
<FeatureIntroBackground />
|
|
|
|
<!-- Contenu principal -->
|
|
<div class="relative z-10 flex flex-col items-center justify-center min-h-screen p-8">
|
|
<!-- Séquence narrative -->
|
|
<Transition name="fade" mode="out-in">
|
|
<div
|
|
v-if="!isChoiceStep"
|
|
:key="currentStep"
|
|
class="max-w-2xl mx-auto text-center"
|
|
>
|
|
<FeatureIntroSequence
|
|
:text="currentText"
|
|
@complete="handleTextComplete"
|
|
@skip="handleTextComplete"
|
|
/>
|
|
|
|
<!-- Bouton continuer -->
|
|
<Transition name="fade">
|
|
<button
|
|
v-if="isTextComplete"
|
|
type="button"
|
|
class="mt-8 px-8 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg hover:opacity-90 transition-opacity focus-visible:outline-2 focus-visible:outline-sky-accent focus-visible:outline-offset-2"
|
|
@click="nextStep"
|
|
>
|
|
{{ isLastTextStep ? $t('intro.startExploring') : $t('intro.continue') }}
|
|
</button>
|
|
</Transition>
|
|
</div>
|
|
|
|
<!-- Choix après l'intro -->
|
|
<div v-else key="choice" class="w-full max-w-3xl mx-auto">
|
|
<FeatureChoiceCards :choice-point="introChoicePoint" />
|
|
</div>
|
|
</Transition>
|
|
|
|
<!-- Bouton skip (toujours visible sauf sur le choix) -->
|
|
<button
|
|
v-if="!isChoiceStep"
|
|
type="button"
|
|
class="absolute bottom-8 right-8 text-sky-text/50 hover:text-sky-text text-sm font-ui underline transition-colors focus-visible:outline-2 focus-visible:outline-sky-accent"
|
|
@click="skipIntro"
|
|
>
|
|
{{ $t('intro.skip') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { CHOICE_POINTS } from '~/types/choice'
|
|
import type { NarratorContext } from '~/composables/useFetchNarratorText'
|
|
|
|
definePageMeta({
|
|
layout: 'adventure',
|
|
})
|
|
|
|
const { t } = useI18n()
|
|
const localePath = useLocalePath()
|
|
const progressionStore = useProgressionStore()
|
|
const { fetchText } = useFetchNarratorText()
|
|
|
|
// Rediriger si pas de héros sélectionné
|
|
onMounted(() => {
|
|
if (!progressionStore.hero) {
|
|
navigateTo(localePath('/'))
|
|
}
|
|
})
|
|
|
|
// Étapes de la séquence
|
|
const steps: (NarratorContext | 'choice')[] = ['intro_sequence_1', 'intro_sequence_2', 'intro_sequence_3', 'choice']
|
|
const currentStepIndex = ref(0)
|
|
|
|
const currentStep = computed(() => steps[currentStepIndex.value])
|
|
const isLastTextStep = computed(() => currentStepIndex.value === steps.length - 2)
|
|
const isChoiceStep = computed(() => currentStep.value === 'choice')
|
|
|
|
// Texte actuel
|
|
const currentText = ref('')
|
|
const isTextComplete = ref(false)
|
|
|
|
// Point de choix pour l'intro
|
|
const introChoicePoint = CHOICE_POINTS.intro_first_choice
|
|
|
|
// Fallback texts
|
|
const fallbackTexts: Record<string, string> = {
|
|
intro_sequence_1: t('intro.fallback.seq1'),
|
|
intro_sequence_2: t('intro.fallback.seq2'),
|
|
intro_sequence_3: t('intro.fallback.seq3'),
|
|
}
|
|
|
|
async function loadCurrentText() {
|
|
if (isChoiceStep.value) return
|
|
|
|
const context = currentStep.value as NarratorContext
|
|
const response = await fetchText(context, progressionStore.hero || undefined)
|
|
|
|
if (response?.text) {
|
|
currentText.value = response.text
|
|
} else {
|
|
// Fallback si l'API n'est pas disponible
|
|
currentText.value = fallbackTexts[context] || ''
|
|
}
|
|
}
|
|
|
|
function handleTextComplete() {
|
|
isTextComplete.value = true
|
|
}
|
|
|
|
function nextStep() {
|
|
if (currentStepIndex.value < steps.length - 1) {
|
|
currentStepIndex.value++
|
|
isTextComplete.value = false
|
|
loadCurrentText()
|
|
}
|
|
}
|
|
|
|
function skipIntro() {
|
|
currentStepIndex.value = steps.length - 1 // Aller directement au choix
|
|
progressionStore.setIntroSeen(true)
|
|
}
|
|
|
|
// Charger le premier texte
|
|
onMounted(() => {
|
|
loadCurrentText()
|
|
})
|
|
|
|
// Marquer l'intro comme vue quand on quitte
|
|
onBeforeUnmount(() => {
|
|
progressionStore.setIntroSeen(true)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.fade-enter-from,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|