# Story 4.2: Intro narrative et premier choix

Status: ready-for-dev

## Story

As a visiteur aventurier,
I want une introduction narrative captivante suivie d'un premier choix,
so that je suis immergé dès le début de l'aventure.

## Acceptance Criteria

1. **Given** le visiteur a sélectionné son héros sur la landing page **When** il commence l'aventure **Then** une séquence d'intro narrative s'affiche avec le narrateur (Le Bug)
2. **And** le texte présente le "héros mystérieux" (le développeur) à découvrir
3. **And** l'effet typewriter anime le texte (skippable par clic/Espace)
4. **And** l'ambiance visuelle est immersive (fond sombre, illustrations)
5. **And** un bouton "Continuer" permet d'avancer
6. **And** à la fin de l'intro, le premier choix binaire s'affiche via `ChoiceCards`
7. **And** le choix propose deux zones à explorer en premier (ex: Projets vs Compétences)
8. **And** le contenu est bilingue (FR/EN) et adapté au héros (vouvoiement/tutoiement)
9. **And** la durée de l'intro est courte (15-30s max, skippable)

## Tasks / Subtasks

- [ ] **Task 1: Créer les textes d'intro dans l'API** (AC: #2, #8)
  - [ ] Ajouter les contextes `intro_sequence_1`, `intro_sequence_2`, `intro_sequence_3` dans narrator_texts
  - [ ] Variantes pour chaque type de héros (vouvoiement/tutoiement)
  - [ ] Textes mystérieux présentant le développeur

- [ ] **Task 2: Créer la page intro** (AC: #1, #4, #9)
  - [ ] Créer `frontend/app/pages/intro.vue`
  - [ ] Rediriger automatiquement depuis landing après choix du héros
  - [ ] Fond sombre avec ambiance mystérieuse
  - [ ] Structure en étapes (séquences de texte)

- [ ] **Task 3: Implémenter la séquence narrative** (AC: #2, #3, #5)
  - [ ] Créer composant `IntroSequence.vue`
  - [ ] Afficher le Bug avec le texte en typewriter
  - [ ] Bouton "Continuer" pour passer à l'étape suivante
  - [ ] Clic/Espace pour skip le typewriter
  - [ ] 3-4 séquences de texte courtes

- [ ] **Task 4: Ajouter les illustrations d'ambiance** (AC: #4)
  - [ ] Illustrations de fond (toiles d'araignée, ombres, code flottant)
  - [ ] Animation subtile sur les éléments de fond
  - [ ] Cohérence avec l'univers de Le Bug

- [ ] **Task 5: Intégrer le premier choix** (AC: #6, #7)
  - [ ] Après la dernière séquence, afficher ChoiceCards
  - [ ] Choix : Projets vs Compétences
  - [ ] La sélection navigue vers la zone choisie

- [ ] **Task 6: Gérer le skip global** (AC: #9)
  - [ ] Bouton discret "Passer l'intro" visible en permanence
  - [ ] Navigation directe vers le choix si skip
  - [ ] Enregistrer dans le store que l'intro a été vue/skip

- [ ] **Task 7: Tests et validation**
  - [ ] Tester le flow complet
  - [ ] Vérifier les 3 types de héros (textes adaptés)
  - [ ] Tester FR et EN
  - [ ] Valider la durée (< 30s)
  - [ ] Tester le skip intro

## Dev Notes

### Textes d'intro (exemples)

```php
// À ajouter dans NarratorTextSeeder.php

// Intro séquence 1 - Recruteur (vouvoiement)
['context' => 'intro_sequence_1', 'text_key' => 'narrator.intro_seq.1.recruteur', 'variant' => 1, 'hero_type' => 'recruteur'],

// Intro séquence 1 - Client/Dev (tutoiement)
['context' => 'intro_sequence_1', 'text_key' => 'narrator.intro_seq.1.casual', 'variant' => 1, 'hero_type' => 'client'],
['context' => 'intro_sequence_1', 'text_key' => 'narrator.intro_seq.1.casual', 'variant' => 1, 'hero_type' => 'dev'],

// Traductions
['key' => 'narrator.intro_seq.1.recruteur', 'fr' => "Bienvenue dans mon domaine, voyageur... Je suis Le Bug, et je vais vous guider dans cette aventure.", 'en' => "Welcome to my domain, traveler... I am The Bug, and I will guide you through this adventure."],
['key' => 'narrator.intro_seq.1.casual', 'fr' => "Hey ! Bienvenue chez moi. Je suis Le Bug, ton guide pour cette aventure.", 'en' => "Hey! Welcome to my place. I'm The Bug, your guide for this adventure."],

['key' => 'narrator.intro_seq.2', 'fr' => "Il y a quelqu'un ici que tu cherches... Un développeur mystérieux qui a créé tout ce que tu vois autour de toi.", 'en' => "There's someone here you're looking for... A mysterious developer who created everything you see around you."],

['key' => 'narrator.intro_seq.3', 'fr' => "Pour le trouver, tu devras explorer ce monde. Chaque zone cache une partie de son histoire. Es-tu prêt ?", 'en' => "To find them, you'll have to explore this world. Each zone hides a part of their story. Are you ready?"],
```

### Page intro.vue

```vue
<!-- frontend/app/pages/intro.vue -->
<script setup lang="ts">
import { CHOICE_POINTS } from '~/types/choice'

const progressionStore = useProgressionStore()
const { t } = useI18n()

// Rediriger si pas de héros sélectionné
if (!progressionStore.heroType) {
  navigateTo('/')
}

// Étapes de la séquence
const steps = ['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)

const { fetchText } = useFetchNarratorText()

async function loadCurrentText() {
  if (isChoiceStep.value) return

  const response = await fetchText(currentStep.value, progressionStore.heroType || undefined)
  currentText.value = response.data.text
}

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
}

// Charger le premier texte
onMounted(() => {
  loadCurrentText()
})

// Marquer l'intro comme vue
onUnmounted(() => {
  progressionStore.setIntroSeen(true)
})
</script>

<template>
  <div class="intro-page min-h-screen bg-sky-dark relative overflow-hidden">
    <!-- Fond d'ambiance -->
    <IntroBackground />

    <!-- 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"
        >
          <IntroSequence
            :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-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors"
              @click="nextStep"
            >
              {{ isLastTextStep ? t('intro.startExploring') : t('intro.continue') }}
            </button>
          </Transition>
        </div>

        <!-- Choix après l'intro -->
        <div v-else class="w-full max-w-3xl mx-auto">
          <ChoiceCards :choice-point="CHOICE_POINTS.intro_first_choice" />
        </div>
      </Transition>

      <!-- Bouton skip (toujours visible) -->
      <button
        v-if="!isChoiceStep"
        type="button"
        class="absolute bottom-8 right-8 text-sky-text-muted hover:text-sky-text text-sm font-ui underline transition-colors"
        @click="skipIntro"
      >
        {{ t('intro.skip') }}
      </button>
    </div>
  </div>
</template>
```

### Composant IntroSequence

```vue
<!-- frontend/app/components/feature/IntroSequence.vue -->
<script setup lang="ts">
const props = defineProps<{
  text: string
}>()

const emit = defineEmits<{
  complete: []
  skip: []
}>()

const progressionStore = useProgressionStore()
const { displayedText, isTyping, isComplete, start, skip } = useTypewriter({
  speed: 35,
  onComplete: () => emit('complete'),
})

// Bug image selon le stage (toujours stage 1 au début)
const bugImage = '/images/bug/bug-stage-1.svg'

// Démarrer le typewriter quand le texte change
watch(() => props.text, (newText) => {
  if (newText) {
    start(newText)
  }
}, { immediate: true })

function handleInteraction() {
  if (isTyping.value) {
    skip()
    emit('skip')
  }
}

function handleKeydown(e: KeyboardEvent) {
  if (e.code === 'Space' || e.code === 'Enter') {
    e.preventDefault()
    handleInteraction()
  }
}
</script>

<template>
  <div
    class="intro-sequence"
    @click="handleInteraction"
    @keydown="handleKeydown"
    tabindex="0"
  >
    <!-- Avatar du Bug -->
    <div class="mb-8">
      <img
        :src="bugImage"
        alt="Le Bug"
        class="w-32 h-32 mx-auto animate-float"
      />
    </div>

    <!-- Texte avec typewriter -->
    <div class="bg-sky-dark-50/80 backdrop-blur rounded-xl p-8 border border-sky-dark-100">
      <p class="font-narrative text-xl md:text-2xl text-sky-text leading-relaxed">
        {{ displayedText }}
        <span
          v-if="isTyping"
          class="inline-block w-0.5 h-6 bg-sky-accent animate-blink ml-1"
        ></span>
      </p>

      <!-- Indication pour skip -->
      <p
        v-if="isTyping"
        class="text-sm text-sky-text-muted mt-4 font-ui"
      >
        {{ $t('narrator.clickToSkip') }}
      </p>
    </div>
  </div>
</template>

<style scoped>
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

.animate-float {
  animation: float 3s ease-in-out infinite;
}

@keyframes blink {
  0%, 50% { opacity: 1; }
  51%, 100% { opacity: 0; }
}

.animate-blink {
  animation: blink 1s infinite;
}

.intro-sequence:focus {
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  .animate-float {
    animation: none;
  }
}
</style>
```

### Composant IntroBackground

```vue
<!-- frontend/app/components/feature/IntroBackground.vue -->
<script setup lang="ts">
const reducedMotion = useReducedMotion()
</script>

<template>
  <div class="intro-background absolute inset-0 overflow-hidden">
    <!-- Gradient de fond -->
    <div class="absolute inset-0 bg-gradient-to-b from-sky-dark via-sky-dark-50 to-sky-dark"></div>

    <!-- Particules flottantes (code fragments) -->
    <div
      v-if="!reducedMotion"
      class="particles absolute inset-0"
    >
      <div
        v-for="i in 20"
        :key="i"
        class="particle absolute text-sky-accent/10 font-mono text-xs"
        :style="{
          left: `${Math.random() * 100}%`,
          top: `${Math.random() * 100}%`,
          animationDelay: `${Math.random() * 5}s`,
          animationDuration: `${10 + Math.random() * 10}s`,
        }"
      >
        {{ ['</', '/>', '{}', '[]', '()', '=>', '&&', '||'][i % 8] }}
      </div>
    </div>

    <!-- Toile d'araignée stylisée (SVG) -->
    <svg
      class="absolute top-0 right-0 w-64 h-64 text-sky-dark-100/30"
      viewBox="0 0 200 200"
    >
      <path
        d="M100,100 L100,0 M100,100 L200,100 M100,100 L100,200 M100,100 L0,100 M100,100 L170,30 M100,100 L170,170 M100,100 L30,170 M100,100 L30,30"
        stroke="currentColor"
        stroke-width="1"
        fill="none"
      />
      <circle cx="100" cy="100" r="30" stroke="currentColor" stroke-width="1" fill="none" />
      <circle cx="100" cy="100" r="60" stroke="currentColor" stroke-width="1" fill="none" />
      <circle cx="100" cy="100" r="90" stroke="currentColor" stroke-width="1" fill="none" />
    </svg>
  </div>
</template>

<style scoped>
@keyframes float-up {
  from {
    transform: translateY(100vh) rotate(0deg);
    opacity: 0;
  }
  10% {
    opacity: 1;
  }
  90% {
    opacity: 1;
  }
  to {
    transform: translateY(-100vh) rotate(360deg);
    opacity: 0;
  }
}

.particle {
  animation: float-up linear infinite;
}
</style>
```

### Clés i18n

**fr.json :**
```json
{
  "intro": {
    "continue": "Continuer",
    "startExploring": "Commencer l'exploration",
    "skip": "Passer l'intro"
  }
}
```

**en.json :**
```json
{
  "intro": {
    "continue": "Continue",
    "startExploring": "Start exploring",
    "skip": "Skip intro"
  }
}
```

### Dépendances

**Cette story nécessite :**
- Story 3.1 : API narrateur (contextes intro_sequence_*)
- Story 3.2 : NarratorBubble et useTypewriter
- Story 4.1 : ChoiceCards pour le premier choix
- Story 1.5 : Landing page (choix du héros)

**Cette story prépare pour :**
- Story 4.3 : Chemins narratifs (suite de l'aventure)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── pages/
│   └── intro.vue                        # CRÉER
└── components/feature/
    ├── IntroSequence.vue                # CRÉER
    └── IntroBackground.vue              # CRÉER
```

**Fichiers à modifier :**
```
api/database/seeders/NarratorTextSeeder.php  # AJOUTER intro_sequence_*
frontend/i18n/fr.json                         # AJOUTER intro.*
frontend/i18n/en.json                         # AJOUTER intro.*
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-4.2]
- [Source: docs/planning-artifacts/ux-design-specification.md#Intro-Sequence]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Onboarding]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Durée intro | 15-30s max (skippable) | Epics |
| Séquences | 3-4 textes courts | Décision technique |
| Premier choix | Projets vs Compétences | Epics |
| Adaptation héros | Vouvoiement/tutoiement | UX Spec |

## Dev Agent Record

### Agent Model Used

{{agent_model_name_version}}

### Debug Log References

### Completion Notes List

### Change Log
| Date | Change | Author |
|------|--------|--------|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |

### File List

