Story 3.3 : Textes narrateur contextuels et arc de révélation - Composable useNarrator.ts avec queue de messages prioritaires - Composable useIdleDetection.ts (détection inactivité 30s) - Plugin narrator-transitions.client.ts (déclencheurs de navigation) - Layout adventure.vue avec NarratorBubble intégré - Store progression: narratorStage devient un getter calculé (0-20-40-60-80%) - Pages projets, competences, temoignages, parcours utilisent layout adventure - Messages: intro, transitions, encouragements 25/50/75%, hints, contact_unlocked Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
Story 3.3: Textes narrateur contextuels et arc de révélation
Status: review
Story
As a visiteur, I want que le narrateur réagisse à mes actions et évolue visuellement, so that l'expérience est personnalisée et le narrateur devient familier.
Acceptance Criteria
- Given le visiteur navigue sur le site When il effectue des actions clés Then le narrateur affiche un message d'accueil à l'arrivée (adapté au héros choisi)
- And des messages de transition s'affichent entre les zones
- And des encouragements apparaissent à 25%, 50%, 75% de progression
- And des indices s'affichent si le visiteur semble inactif (> 30s sans action)
- And un message spécial "Bienvenue à nouveau" s'affiche si progression existante détectée
- And le message de déblocage du contact s'affiche après 2 zones visitées
- Given le visiteur progresse dans l'exploration When le
completionPercentatteint certains seuils Then lenarratorStagedu store est mis à jour (1→5) - And l'apparence du Bug évolue : silhouette sombre (1) → forme vague (2) → pattes visibles (3) → araignée reconnaissable (4) → mascotte complète révélée (5)
- And le ton du narrateur évolue de mystérieux à complice
Tasks / Subtasks
-
Task 1: Créer le composable useNarrator (AC: #1, #2, #3, #4, #5, #6)
- Créer
frontend/app/composables/useNarrator.ts - Centraliser la logique d'affichage du narrateur
- Exposer les méthodes : showIntro, showTransition, showEncouragement, showHint, showWelcomeBack, showContactUnlocked
- Gérer la queue de messages (ne pas interrompre un message en cours)
- Intégrer le composable useFetchNarratorText
- Créer
-
Task 2: Implémenter les déclencheurs de transition (AC: #2)
- Déclencher sur navigation vers /projets (transition_projects)
- Déclencher sur navigation vers /competences (transition_skills)
- Déclencher sur navigation vers /temoignages (transition_testimonials)
- Déclencher sur navigation vers /parcours (transition_journey)
- Utiliser un plugin Nuxt ou watcher sur la route
-
Task 3: Implémenter la détection d'inactivité (AC: #4)
- Créer
frontend/app/composables/useIdleDetection.ts - Détecter l'absence d'interaction > 30 secondes
- Écouter mouse, keyboard, touch, scroll
- Déclencher
showHint()quand idle détecté - Ne pas répéter les hints trop souvent (cooldown de 2min)
- Créer
-
Task 4: Implémenter les encouragements basés sur la progression (AC: #3)
- Watcher sur
completionPercentdu store - Déclencher à 25%, 50%, 75%
- Garder en mémoire les seuils déjà atteints (ne pas répéter)
- Watcher sur
-
Task 5: Implémenter l'arc de révélation du Bug (AC: #7, #8, #9)
- Définir les seuils de progression pour chaque stage :
- Stage 1 : 0-19%
- Stage 2 : 20-39%
- Stage 3 : 40-59%
- Stage 4 : 60-79%
- Stage 5 : 80-100%
- Mettre à jour
narratorStagedans le store (getter calculé) - L'image du Bug se met à jour automatiquement via NarratorBubble
- Définir les seuils de progression pour chaque stage :
-
Task 6: Implémenter le message "Bienvenue à nouveau" (AC: #5)
- Détecter au chargement si
visitedSectionsn'est pas vide (progression existante) - Afficher le message
welcome_backdans ce cas - Sinon afficher le message
intronormal
- Détecter au chargement si
-
Task 7: Implémenter le message de déblocage contact (AC: #6)
- Watcher sur
contactUnlockeddu store - Quand passe à
true, affichercontact_unlocked
- Watcher sur
-
Task 8: Intégrer dans le layout principal
- Ajouter le NarratorBubble dans adventure.vue
- Initialiser useNarrator dans le layout
- Gérer l'état visible/hidden du narrateur
-
Task 9: Tests et validation
- Tester le message d'accueil adapté au héros
- Tester les transitions entre pages
- Vérifier les encouragements à 25/50/75%
- Tester la détection d'inactivité
- Valider l'évolution du Bug (5 stages)
- Tester le "Bienvenue à nouveau"
Dev Notes
Composable useNarrator
// frontend/app/composables/useNarrator.ts
interface NarratorMessage {
context: string
priority: number
}
export function useNarrator() {
const { fetchText } = useFetchNarratorText()
const progressionStore = useProgressionStore()
const isVisible = ref(false)
const currentMessage = ref('')
const messageQueue = ref<NarratorMessage[]>([])
const isProcessing = ref(false)
// Seuils d'encouragement déjà affichés
const shownEncouragements = ref<Set<number>>(new Set())
// Cooldown pour les hints
const lastHintTime = ref(0)
const HINT_COOLDOWN = 120000 // 2 minutes
async function queueMessage(context: string, priority: number = 5) {
messageQueue.value.push({ context, priority })
messageQueue.value.sort((a, b) => b.priority - a.priority)
if (!isProcessing.value) {
processQueue()
}
}
async function processQueue() {
if (messageQueue.value.length === 0) {
isProcessing.value = false
return
}
isProcessing.value = true
const next = messageQueue.value.shift()!
try {
const response = await fetchText(next.context, progressionStore.heroType)
currentMessage.value = response.data.text
isVisible.value = true
} catch (error) {
console.error('Failed to fetch narrator text:', error)
processQueue() // Passer au suivant en cas d'erreur
}
}
function hide() {
isVisible.value = false
// Attendre la fin de l'animation avant de traiter le suivant
setTimeout(() => {
processQueue()
}, 300)
}
// === Méthodes publiques ===
async function showIntro() {
// Vérifier si le visiteur revient
if (progressionStore.visitedSections.length > 0) {
await queueMessage('welcome_back', 10)
} else {
await queueMessage('intro', 10)
}
}
async function showTransition(zone: 'projects' | 'skills' | 'testimonials' | 'journey') {
const contextMap = {
projects: 'transition_projects',
skills: 'transition_skills',
testimonials: 'transition_testimonials',
journey: 'transition_journey',
}
await queueMessage(contextMap[zone], 7)
}
async function showEncouragement(percent: number) {
// Ne pas répéter les encouragements
if (shownEncouragements.value.has(percent)) return
let context: string | null = null
if (percent >= 75 && !shownEncouragements.value.has(75)) {
context = 'encouragement_75'
shownEncouragements.value.add(75)
} else if (percent >= 50 && !shownEncouragements.value.has(50)) {
context = 'encouragement_50'
shownEncouragements.value.add(50)
} else if (percent >= 25 && !shownEncouragements.value.has(25)) {
context = 'encouragement_25'
shownEncouragements.value.add(25)
}
if (context) {
await queueMessage(context, 5)
}
}
async function showHint() {
const now = Date.now()
if (now - lastHintTime.value < HINT_COOLDOWN) return
lastHintTime.value = now
await queueMessage('hint', 3)
}
async function showContactUnlocked() {
await queueMessage('contact_unlocked', 8)
}
async function showWelcomeBack() {
await queueMessage('welcome_back', 10)
}
return {
isVisible,
currentMessage,
hide,
showIntro,
showTransition,
showEncouragement,
showHint,
showContactUnlocked,
showWelcomeBack,
}
}
Composable useIdleDetection
// frontend/app/composables/useIdleDetection.ts
export interface UseIdleDetectionOptions {
timeout?: number // ms avant de considérer comme idle
onIdle?: () => void
}
export function useIdleDetection(options: UseIdleDetectionOptions = {}) {
const { timeout = 30000, onIdle } = options
const isIdle = ref(false)
let timeoutId: ReturnType<typeof setTimeout> | null = null
function resetTimer() {
isIdle.value = false
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
isIdle.value = true
onIdle?.()
}, timeout)
}
const events = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart']
onMounted(() => {
events.forEach(event => {
window.addEventListener(event, resetTimer, { passive: true })
})
resetTimer() // Démarrer le timer
})
onUnmounted(() => {
events.forEach(event => {
window.removeEventListener(event, resetTimer)
})
if (timeoutId) {
clearTimeout(timeoutId)
}
})
return { isIdle }
}
Logique de l'arc de révélation (dans useProgressionStore)
// Ajouter dans frontend/app/stores/progression.ts
// Seuils pour les stages du Bug
const NARRATOR_STAGE_THRESHOLDS = [0, 20, 40, 60, 80] // 5 stages
function calculateNarratorStage(percent: number): number {
for (let i = NARRATOR_STAGE_THRESHOLDS.length - 1; i >= 0; i--) {
if (percent >= NARRATOR_STAGE_THRESHOLDS[i]) {
return i + 1 // Stages 1-5
}
}
return 1
}
// Dans le store
export const useProgressionStore = defineStore('progression', () => {
// ... autres propriétés existantes ...
const narratorStage = computed(() => {
return calculateNarratorStage(completionPercent.value)
})
return {
// ... autres exports ...
narratorStage,
}
})
Plugin de navigation pour les transitions
// frontend/app/plugins/narrator-transitions.client.ts
export default defineNuxtPlugin((nuxtApp) => {
const narrator = useNarrator()
const router = useRouter()
const progressionStore = useProgressionStore()
// Map des routes vers les contextes de transition
const routeContextMap: Record<string, 'projects' | 'skills' | 'testimonials' | 'journey'> = {
'/projets': 'projects',
'/en/projects': 'projects',
'/competences': 'skills',
'/en/skills': 'skills',
'/temoignages': 'testimonials',
'/en/testimonials': 'testimonials',
'/parcours': 'journey',
'/en/journey': 'journey',
}
// Sections déjà annoncées (pour ne pas répéter)
const announcedSections = new Set<string>()
router.afterEach((to) => {
const zone = routeContextMap[to.path]
if (zone && !announcedSections.has(zone)) {
announcedSections.add(zone)
narrator.showTransition(zone)
}
})
// Watcher sur completionPercent pour les encouragements
watch(
() => progressionStore.completionPercent,
(percent) => {
narrator.showEncouragement(percent)
}
)
// Watcher sur contactUnlocked
watch(
() => progressionStore.contactUnlocked,
(unlocked, wasUnlocked) => {
if (unlocked && !wasUnlocked) {
narrator.showContactUnlocked()
}
}
)
})
Intégration dans le layout
<!-- frontend/app/layouts/adventure.vue -->
<script setup lang="ts">
const narrator = useNarrator()
// Détection d'inactivité
useIdleDetection({
timeout: 30000,
onIdle: () => {
narrator.showHint()
}
})
// Afficher l'intro au montage
onMounted(() => {
// Délai pour laisser la page se charger
setTimeout(() => {
narrator.showIntro()
}, 1000)
})
</script>
<template>
<div class="adventure-layout">
<slot />
<!-- Narrateur -->
<NarratorBubble
:message="narrator.currentMessage.value"
:visible="narrator.isVisible.value"
@close="narrator.hide()"
/>
</div>
</template>
Tableau des stages du Bug
| Stage | Progression | Apparence | Ton du narrateur |
|---|---|---|---|
| 1 | 0-19% | Silhouette sombre floue | Mystérieux, énigmatique |
| 2 | 20-39% | Forme vague avec yeux brillants | Curieux, observateur |
| 3 | 40-59% | Pattes visibles, forme d'araignée | Encourageant, guide |
| 4 | 60-79% | Araignée reconnaissable | Amical, complice |
| 5 | 80-100% | Mascotte complète révélée | Chaleureux, félicitations |
Dépendances
Cette story nécessite :
- Story 3.1 : API narrateur (contextes et textes)
- Story 3.2 : Composant NarratorBubble
- Story 1.6 : Store Pinia (pour progression et heroType)
Cette story prépare pour :
- Story 3.5 : Logique de progression (déclenche les messages)
- Story 4.2 : Intro narrative (utilise useNarrator)
Project Structure Notes
Fichiers à créer :
frontend/app/
├── composables/
│ ├── useNarrator.ts # CRÉER
│ └── useIdleDetection.ts # CRÉER
├── plugins/
│ └── narrator-transitions.client.ts # CRÉER
└── layouts/
└── adventure.vue # CRÉER ou MODIFIER
Fichiers à modifier :
frontend/app/stores/progression.ts # AJOUTER narratorStage computed
References
- [Source: docs/planning-artifacts/epics.md#Story-3.3]
- [Source: docs/planning-artifacts/ux-design-specification.md#Narrator-Revelation-Arc]
- [Source: docs/planning-artifacts/ux-design-specification.md#Narrator-Contexts]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Arc-Revelation]
Technical Requirements
| Requirement | Value | Source |
|---|---|---|
| Stages du Bug | 5 (silhouette → mascotte) | UX Spec |
| Seuils progression | 0/20/40/60/80% | Décision technique |
| Timeout inactivité | 30 secondes | Epics |
| Cooldown hints | 2 minutes | Décision technique |
| Contextes transitions | 4 zones principales | 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 |