# Story 4.7: Révélation "Monde de Code"

Status: ready-for-dev

## Story

As a visiteur ayant complété le parcours,
I want vivre un moment waouh de révélation finale,
so that la découverte du développeur est mémorable.

## Acceptance Criteria

1. **Given** le visiteur accède à la zone Contact (après challenge ou skip) **When** la révélation se déclenche **Then** une transition immersive mène vers le "Monde de Code"
2. **And** un paysage composé de blocs de code ASCII art s'affiche (montagnes, arbres, ville en code)
3. **And** le code scroll/apparaît progressivement (animation)
4. **And** l'avatar illustré de Célian est révélé au centre du monde de code
5. **And** le narrateur (Le Bug) commente : "Tu l'as trouvé !"
6. **And** le message "Tu m'as trouvé !" s'affiche avec effet de célébration
7. **And** sur mobile, une version allégée mais émotionnellement équivalente s'affiche
8. **And** `prefers-reduced-motion` affiche une version statique
9. **And** une description alternative est disponible pour les screen readers
10. **And** un bouton permet de continuer vers le formulaire de contact

## Tasks / Subtasks

- [ ] **Task 1: Créer la page révélation** (AC: #1, #10)
  - [ ] Créer `frontend/app/pages/revelation.vue`
  - [ ] Vérifier que le contact est débloqué
  - [ ] Structure en phases : transition → monde de code → avatar → message

- [ ] **Task 2: Créer le composant CodeWorld** (AC: #2, #3)
  - [ ] Créer `frontend/app/components/feature/CodeWorld.vue`
  - [ ] ASCII art représentant un paysage (montagnes, arbres, soleil)
  - [ ] Animation de révélation ligne par ligne
  - [ ] Couleurs syntaxiques (comme du code)

- [ ] **Task 3: Créer l'ASCII art du paysage**
  - [ ] Montagnes en caractères (`/\`, `^`, etc.)
  - [ ] Arbres stylisés (`{}`, `[]`)
  - [ ] Soleil ou étoiles
  - [ ] Personnage au centre

- [ ] **Task 4: Révéler l'avatar de Célian** (AC: #4)
  - [ ] Image illustrée de Célian
  - [ ] Animation d'apparition (fade + scale)
  - [ ] Position centrale sur le monde de code

- [ ] **Task 5: Message du narrateur** (AC: #5)
  - [ ] Le Bug s'exclame "Tu l'as trouvé !"
  - [ ] Utiliser NarratorBubble ou message intégré
  - [ ] Ton enthousiaste et célébratoire

- [ ] **Task 6: Message de Célian** (AC: #6)
  - [ ] "Tu m'as trouvé !" avec effet typewriter
  - [ ] Animation de célébration autour
  - [ ] Signature de Célian

- [ ] **Task 7: Version mobile** (AC: #7)
  - [ ] ASCII art simplifié ou image de remplacement
  - [ ] Mêmes éléments clés : avatar, message, émotion
  - [ ] Performance optimisée

- [ ] **Task 8: Accessibilité** (AC: #8, #9)
  - [ ] Respecter prefers-reduced-motion (version statique)
  - [ ] Description alternative pour screen readers
  - [ ] aria-label descriptif

- [ ] **Task 9: Tests et validation**
  - [ ] Tester l'animation complète
  - [ ] Vérifier la version mobile
  - [ ] Tester prefers-reduced-motion
  - [ ] Valider l'accessibilité

## Dev Notes

### ASCII Art du Monde de Code

```
                    *  .  *
           *   .        .   *
      .           ___           .
   *      .     /     \    *
         .    /   ^   \      .   *
    *       /    /^\    \   *
  .       /____/   \____\      .
      *  |    |     |    |  *
   .     |    |     |    |     .
  _______|    |_____|    |_______
 /       |    |     |    |       \
{  Vue  }| TS |{PHP}| DB |{Nuxt}
 \_______________________/
    ||     ||     ||
   {  }   {  }   {  }
    ||     ||     ||
 ___||_____||_____||___
|         YOU         |
|    FOUND ME! 🎉     |
|_____________________|
```

### Page revelation.vue

```vue
<!-- frontend/app/pages/revelation.vue -->
<script setup lang="ts">
const { t } = useI18n()
const router = useRouter()
const progressionStore = useProgressionStore()
const narrator = useNarrator()
const reducedMotion = useReducedMotion()

// Vérifier que le contact est débloqué
if (!progressionStore.contactUnlocked) {
  navigateTo('/')
}

// Phases de la révélation
type Phase = 'transition' | 'codeworld' | 'avatar' | 'message' | 'complete'
const currentPhase = ref<Phase>('transition')

// Progression des phases
async function advancePhase() {
  const phases: Phase[] = ['transition', 'codeworld', 'avatar', 'message', 'complete']
  const currentIndex = phases.indexOf(currentPhase.value)

  if (currentIndex < phases.length - 1) {
    currentPhase.value = phases[currentIndex + 1]

    // Actions spécifiques par phase
    if (currentPhase.value === 'avatar') {
      await narrator.showMessage('revelation_found')
    }
  }
}

// Démarrer la séquence
onMounted(() => {
  if (reducedMotion.value) {
    // Version statique : aller directement à complete
    currentPhase.value = 'complete'
  } else {
    // Animation : transition vers codeworld après 1.5s
    setTimeout(() => {
      advancePhase()
    }, 1500)
  }
})

function goToContact() {
  router.push('/contact')
}
</script>

<template>
  <div class="revelation-page min-h-screen bg-sky-dark overflow-hidden">
    <!-- Screen reader description -->
    <p class="sr-only">
      {{ t('revelation.srDescription') }}
    </p>

    <!-- Phase : Transition -->
    <Transition name="fade">
      <div
        v-if="currentPhase === 'transition'"
        class="fixed inset-0 flex items-center justify-center bg-black z-50"
      >
        <p class="font-narrative text-2xl text-sky-text animate-pulse">
          {{ t('revelation.transition') }}
        </p>
      </div>
    </Transition>

    <!-- Phase : Code World -->
    <div
      v-show="currentPhase !== 'transition'"
      class="relative min-h-screen flex flex-col items-center justify-center p-4"
    >
      <!-- ASCII Code World -->
      <CodeWorld
        :animate="currentPhase === 'codeworld'"
        @complete="advancePhase"
        class="mb-8"
      />

      <!-- Avatar de Célian -->
      <Transition name="scale-fade">
        <div
          v-if="['avatar', 'message', 'complete'].includes(currentPhase)"
          class="relative"
        >
          <img
            src="/images/avatar-celian.svg"
            alt="Célian"
            class="w-32 h-32 md:w-48 md:h-48 rounded-full border-4 border-sky-accent shadow-2xl shadow-sky-accent/30"
          />

          <!-- Sparkles autour -->
          <div class="absolute inset-0 -m-4">
            <span
              v-for="i in 8"
              :key="i"
              class="absolute text-xl animate-pulse"
              :style="{
                top: `${50 + 45 * Math.sin(i * Math.PI / 4)}%`,
                left: `${50 + 45 * Math.cos(i * Math.PI / 4)}%`,
                transform: 'translate(-50%, -50%)',
                animationDelay: `${i * 100}ms`,
              }"
            >
              ✨
            </span>
          </div>
        </div>
      </Transition>

      <!-- Message "Tu m'as trouvé !" -->
      <Transition name="slide-up">
        <div
          v-if="['message', 'complete'].includes(currentPhase)"
          class="mt-8 text-center"
        >
          <h1 class="text-4xl md:text-5xl font-ui font-bold text-sky-accent mb-4">
            {{ t('revelation.foundMe') }}
          </h1>

          <p class="font-narrative text-xl text-sky-text mb-2">
            {{ t('revelation.greeting') }}
          </p>

          <p class="font-ui text-sky-text-muted">
            — Célian, {{ t('revelation.title') }}
          </p>
        </div>
      </Transition>

      <!-- Bouton continuer -->
      <Transition name="fade">
        <button
          v-if="currentPhase === 'complete'"
          type="button"
          class="mt-12 px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-xl hover:bg-sky-accent/90 transition-colors shadow-lg shadow-sky-accent/30"
          @click="goToContact"
        >
          {{ t('revelation.contactMe') }}
        </button>
      </Transition>
    </div>

    <!-- Version reduced-motion -->
    <div
      v-if="reducedMotion && currentPhase === 'complete'"
      class="fixed inset-0 flex flex-col items-center justify-center p-8 bg-sky-dark"
    >
      <img
        src="/images/avatar-celian.svg"
        alt="Célian"
        class="w-32 h-32 rounded-full border-4 border-sky-accent mb-8"
      />

      <h1 class="text-3xl font-ui font-bold text-sky-accent mb-4">
        {{ t('revelation.foundMe') }}
      </h1>

      <p class="font-narrative text-lg text-sky-text text-center mb-8">
        {{ t('revelation.greeting') }}
      </p>

      <button
        type="button"
        class="px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-xl"
        @click="goToContact"
      >
        {{ t('revelation.contactMe') }}
      </button>
    </div>
  </div>
</template>

<style scoped>
.scale-fade-enter-active,
.scale-fade-leave-active {
  transition: all 0.8s ease;
}

.scale-fade-enter-from {
  opacity: 0;
  transform: scale(0.5);
}

.slide-up-enter-active,
.slide-up-leave-active {
  transition: all 0.6s ease;
}

.slide-up-enter-from {
  opacity: 0;
  transform: translateY(30px);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
```

### Composant CodeWorld

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

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

const reducedMotion = useReducedMotion()

// ASCII Art du monde de code
const asciiArt = `
          *  .  *       .    *
     *   .        .   *    .
  .           ___           .
       .    /     \\    *
  *       /   ^   \\      .   *
       /    /^\\    \\   *
     /____/   \\____\\      .
 *  |    |     |    |  *
    |    |     |    |     .
____|    |_____|    |_______
    |    |     |    |
{Vue}| TS |{PHP}| DB |{Nuxt}
____________________________
   ||     ||     ||
  {  }   {  }   {  }
   ||     ||     ||
`.trim()

const lines = asciiArt.split('\n')
const visibleLines = ref(reducedMotion.value ? lines.length : 0)

// Animation ligne par ligne
watch(() => props.animate, (shouldAnimate) => {
  if (shouldAnimate && !reducedMotion.value) {
    animateLines()
  }
})

function animateLines() {
  const interval = setInterval(() => {
    if (visibleLines.value < lines.length) {
      visibleLines.value++
    } else {
      clearInterval(interval)
      setTimeout(() => {
        emit('complete')
      }, 500)
    }
  }, 100)
}

// Coloration syntaxique simple
function colorize(line: string): string {
  return line
    .replace(/{(\w+)}/g, '<span class="text-green-400">{$1}</span>')
    .replace(/\|/g, '<span class="text-sky-accent">|</span>')
    .replace(/\*/g, '<span class="text-yellow-400">*</span>')
    .replace(/\./g, '<span class="text-blue-400">.</span>')
}
</script>

<template>
  <div
    class="code-world font-mono text-xs md:text-sm text-sky-text-muted leading-tight"
    role="img"
    :aria-label="$t('revelation.codeWorldAlt')"
  >
    <pre class="overflow-hidden"><code><template v-for="(line, index) in lines" :key="index"><span
          v-if="index < visibleLines"
          v-html="colorize(line)"
          class="block"
        ></span></template></code></pre>
  </div>
</template>

<style scoped>
.code-world {
  text-shadow: 0 0 10px rgba(250, 120, 79, 0.3);
}
</style>
```

### Clés i18n

**fr.json :**
```json
{
  "revelation": {
    "transition": "Le voilà...",
    "foundMe": "Tu m'as trouvé !",
    "greeting": "Bienvenue dans mon monde de code. Je suis Célian, le développeur que tu cherchais depuis le début.",
    "title": "Développeur Web Fullstack",
    "contactMe": "Me contacter",
    "codeWorldAlt": "Un paysage stylisé composé de caractères de code, représentant l'univers du développeur",
    "srDescription": "Vous avez découvert le développeur ! Célian vous accueille dans son monde de code."
  }
}
```

**en.json :**
```json
{
  "revelation": {
    "transition": "There he is...",
    "foundMe": "You found me!",
    "greeting": "Welcome to my world of code. I'm Célian, the developer you've been looking for all along.",
    "title": "Fullstack Web Developer",
    "contactMe": "Contact me",
    "codeWorldAlt": "A stylized landscape made of code characters, representing the developer's universe",
    "srDescription": "You discovered the developer! Célian welcomes you to his world of code."
  }
}
```

### Dépendances

**Cette story nécessite :**
- Story 3.5 : Store de progression (contactUnlocked)
- Story 3.2 : useReducedMotion
- Story 3.3 : useNarrator (révélation)

**Cette story prépare pour :**
- Story 4.8 : Page contact (destination finale)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── pages/
│   └── revelation.vue                   # CRÉER
├── components/feature/
│   └── CodeWorld.vue                    # CRÉER
└── public/images/
    └── avatar-celian.svg                # CRÉER (asset)
```

**Fichiers à modifier :**
```
frontend/i18n/fr.json                    # AJOUTER revelation.*
frontend/i18n/en.json                    # AJOUTER revelation.*
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-4.7]
- [Source: docs/planning-artifacts/ux-design-specification.md#Revelation]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Revelation]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| ASCII Art | Paysage stylisé | Epics |
| Avatar | Image de Célian | Epics |
| Message | "Tu m'as trouvé !" | Epics |
| Accessibilité | prefers-reduced-motion, aria | Epics |

## 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

