Files
Portfolio-Game/docs/implementation-artifacts/4-7-revelation-monde-de-code.md
skycel ec1ae92799 🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
Setup complet de l'infrastructure projet :
- Frontend Nuxt 4 (SSR, TypeScript, i18n, Pinia, TailwindCSS)
- Backend Laravel 12 API-only avec middleware X-API-Key et CORS
- Design tokens (sky-dark, sky-accent, sky-text) et polices (Merriweather, Inter)
- Documentation planning et implementation artifacts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 02:08:56 +01:00

14 KiB

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

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

<!-- 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 :

{
  "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 :

{
  "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