Files
Portfolio-Game/docs/implementation-artifacts/4-3-chemins-narratifs-differencies.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

424 lines
14 KiB
Markdown

# Story 4.3: Chemins narratifs différenciés
Status: ready-for-dev
## Story
As a visiteur,
I want que mes choix aient un impact visible sur mon parcours,
so that je sens que mon expérience est vraiment personnalisée.
## Acceptance Criteria
1. **Given** le visiteur fait des choix tout au long de l'aventure **When** il navigue entre les zones **Then** 2-3 points de choix binaires créent 4-8 parcours possibles
2. **And** chaque choix est enregistré dans `choices` du store
3. **And** l'ordre suggéré des zones varie selon le chemin choisi
4. **And** les textes du narrateur s'adaptent au chemin (transitions contextuelles)
5. **And** tous les chemins permettent de visiter tout le contenu
6. **And** tous les chemins mènent au contact (pas de "mauvais" choix)
7. **And** le `currentPath` du store reflète le chemin actuel
8. **And** à la fin de chaque zone, le narrateur propose un choix vers la suite
## Tasks / Subtasks
- [ ] **Task 1: Définir l'arbre des chemins** (AC: #1, #5, #6)
- [ ] Créer `frontend/app/data/narrativePaths.ts`
- [ ] Définir 2-3 points de choix créant 4-8 parcours
- [ ] S'assurer que tous les chemins visitent toutes les zones
- [ ] S'assurer que tous les chemins mènent au contact
- [ ] **Task 2: Créer le composable useNarrativePath** (AC: #2, #3, #7)
- [ ] Créer `frontend/app/composables/useNarrativePath.ts`
- [ ] Calculer le chemin actuel basé sur les choix
- [ ] Exposer la prochaine zone suggérée
- [ ] Exposer les zones restantes dans l'ordre
- [ ] **Task 3: Ajouter les textes de transition contextuels** (AC: #4)
- [ ] Créer des contextes spécifiques : `transition_after_projects_to_skills`, etc.
- [ ] Variantes selon le chemin pris
- [ ] Commentaires du narrateur sur les choix précédents
- [ ] **Task 4: Intégrer les choix après chaque zone** (AC: #8)
- [ ] Composant `ZoneEndChoice.vue` affiché à la fin de chaque page de zone
- [ ] Proposer les options de destination selon le chemin
- [ ] Utiliser ChoiceCards pour la présentation
- [ ] **Task 5: Mettre à jour le store** (AC: #2, #7)
- [ ] Ajouter `currentPath` computed au store
- [ ] Ajouter `suggestedNextZone` computed
- [ ] Méthode pour obtenir le choix à un point donné
- [ ] **Task 6: Créer l'API pour les transitions contextuelles** (AC: #4)
- [ ] Endpoint `/api/narrator/transition-contextual`
- [ ] Paramètres : from_zone, to_zone, path_choices
- [ ] Retourner un texte adapté au contexte
- [ ] **Task 7: Tests et validation**
- [ ] Tester tous les chemins possibles (4-8)
- [ ] Vérifier que tous mènent au contact
- [ ] Valider les textes contextuels
- [ ] Tester la suggestion de zone suivante
## Dev Notes
### Arbre des chemins narratifs
```typescript
// frontend/app/data/narrativePaths.ts
// Points de choix dans l'aventure
export const NARRATIVE_CHOICE_POINTS = {
// Point 1 : Après l'intro
intro: {
id: 'intro',
options: ['projects', 'skills'],
},
// Point 2 : Après la première zone
after_first_zone: {
id: 'after_first_zone',
options: ['testimonials', 'journey'],
},
// Point 3 : Après la deuxième zone
after_second_zone: {
id: 'after_second_zone',
// Les options dépendent de ce qui reste
},
}
// Chemins possibles (4-8 combinaisons)
// Format : intro_choice -> after_first -> after_second -> contact
export const NARRATIVE_PATHS = [
// Chemin 1 : Projets → Témoignages → Compétences → Parcours → Contact
['projects', 'testimonials', 'skills', 'journey', 'contact'],
// Chemin 2 : Projets → Témoignages → Parcours → Compétences → Contact
['projects', 'testimonials', 'journey', 'skills', 'contact'],
// Chemin 3 : Projets → Parcours → Témoignages → Compétences → Contact
['projects', 'journey', 'testimonials', 'skills', 'contact'],
// Chemin 4 : Projets → Parcours → Compétences → Témoignages → Contact
['projects', 'journey', 'skills', 'testimonials', 'contact'],
// Chemin 5 : Compétences → Témoignages → Projets → Parcours → Contact
['skills', 'testimonials', 'projects', 'journey', 'contact'],
// Chemin 6 : Compétences → Témoignages → Parcours → Projets → Contact
['skills', 'testimonials', 'journey', 'projects', 'contact'],
// Chemin 7 : Compétences → Parcours → Témoignages → Projets → Contact
['skills', 'journey', 'testimonials', 'projects', 'contact'],
// Chemin 8 : Compétences → Parcours → Projets → Témoignages → Contact
['skills', 'journey', 'projects', 'testimonials', 'contact'],
]
// Mapper zone key -> route
export const ZONE_ROUTES: Record<string, { fr: string; en: string }> = {
projects: { fr: '/projets', en: '/en/projects' },
skills: { fr: '/competences', en: '/en/skills' },
testimonials: { fr: '/temoignages', en: '/en/testimonials' },
journey: { fr: '/parcours', en: '/en/journey' },
contact: { fr: '/contact', en: '/en/contact' },
}
```
### Composable useNarrativePath
```typescript
// frontend/app/composables/useNarrativePath.ts
import { NARRATIVE_PATHS, ZONE_ROUTES } from '~/data/narrativePaths'
export function useNarrativePath() {
const progressionStore = useProgressionStore()
const { locale } = useI18n()
// Déterminer le chemin actuel basé sur les choix
const currentPath = computed(() => {
const choices = progressionStore.choices
// Trouver le premier choix (intro)
const introChoice = choices.find(c => c.id === 'intro_first_choice')
if (!introChoice) return null
const startZone = introChoice.value === 'choice_projects_first' ? 'projects' : 'skills'
// Filtrer les chemins qui commencent par cette zone
let possiblePaths = NARRATIVE_PATHS.filter(path => path[0] === startZone)
// Affiner avec les choix suivants
const afterFirstChoice = choices.find(c => c.id === 'after_first_zone')
if (afterFirstChoice && possiblePaths.length > 1) {
const secondZone = afterFirstChoice.value.includes('testimonials') ? 'testimonials' : 'journey'
possiblePaths = possiblePaths.filter(path => path[1] === secondZone)
}
return possiblePaths[0] || null
})
// Zone actuelle basée sur la route
const currentZone = computed(() => {
const route = useRoute()
const path = route.path.toLowerCase()
for (const [zone, routes] of Object.entries(ZONE_ROUTES)) {
if (path.includes(routes.fr.slice(1)) || path.includes(routes.en.slice(4))) {
return zone
}
}
return null
})
// Index de la zone actuelle dans le chemin
const currentZoneIndex = computed(() => {
if (!currentPath.value || !currentZone.value) return -1
return currentPath.value.indexOf(currentZone.value)
})
// Prochaine zone suggérée
const suggestedNextZone = computed(() => {
if (!currentPath.value || currentZoneIndex.value === -1) return null
const nextIndex = currentZoneIndex.value + 1
if (nextIndex >= currentPath.value.length) return null
return currentPath.value[nextIndex]
})
// Zones restantes à visiter
const remainingZones = computed(() => {
if (!currentPath.value) return []
const visited = progressionStore.visitedSections
return currentPath.value.filter(zone =>
zone !== 'contact' && !visited.includes(zone as any)
)
})
// Obtenir la route pour une zone
function getZoneRoute(zone: string): string {
const routes = ZONE_ROUTES[zone]
if (!routes) return '/'
return locale.value === 'fr' ? routes.fr : routes.en
}
// Générer le choix pour après la zone actuelle
function getNextChoicePoint() {
if (!remainingZones.value.length) {
// Plus de zones, aller au contact
return {
id: 'go_to_contact',
choices: [
{
id: 'contact',
textFr: 'Rencontrer le développeur',
textEn: 'Meet the developer',
icon: '📧',
destination: getZoneRoute('contact'),
zoneColor: '#fa784f',
},
],
}
}
// Proposer les 2 prochaines zones
const nextTwo = remainingZones.value.slice(0, 2)
return {
id: `after_${currentZone.value}`,
questionFr: 'Où vas-tu ensuite ?',
questionEn: 'Where to next?',
choices: nextTwo.map(zone => ({
id: `choice_${zone}`,
textFr: getZoneLabel(zone, 'fr'),
textEn: getZoneLabel(zone, 'en'),
icon: getZoneIcon(zone),
destination: getZoneRoute(zone),
zoneColor: getZoneColor(zone),
})),
}
}
return {
currentPath,
currentZone,
suggestedNextZone,
remainingZones,
getZoneRoute,
getNextChoicePoint,
}
}
// Helpers
function getZoneLabel(zone: string, locale: string): string {
const labels: Record<string, { fr: string; en: string }> = {
projects: { fr: 'Découvrir les créations', en: 'Discover the creations' },
skills: { fr: 'Explorer les compétences', en: 'Explore the skills' },
testimonials: { fr: 'Écouter les témoignages', en: 'Listen to testimonials' },
journey: { fr: 'Suivre le parcours', en: 'Follow the journey' },
}
return labels[zone]?.[locale] || zone
}
function getZoneIcon(zone: string): string {
const icons: Record<string, string> = {
projects: '💻',
skills: '⚡',
testimonials: '💬',
journey: '📍',
}
return icons[zone] || '?'
}
function getZoneColor(zone: string): string {
const colors: Record<string, string> = {
projects: '#3b82f6',
skills: '#10b981',
testimonials: '#f59e0b',
journey: '#8b5cf6',
}
return colors[zone] || '#fa784f'
}
```
### Composant ZoneEndChoice
```vue
<!-- frontend/app/components/feature/ZoneEndChoice.vue -->
<script setup lang="ts">
const { getNextChoicePoint, remainingZones } = useNarrativePath()
const narrator = useNarrator()
const choicePoint = computed(() => getNextChoicePoint())
// Afficher un message du narrateur avant le choix
onMounted(async () => {
if (remainingZones.value.length > 0) {
await narrator.showTransitionChoice()
} else {
await narrator.showContactReady()
}
})
</script>
<template>
<div class="zone-end-choice py-16 px-4 border-t border-sky-dark-100 mt-16">
<div class="max-w-2xl mx-auto">
<!-- Message narratif -->
<p class="font-narrative text-xl text-sky-text text-center mb-8 italic">
{{ $t('narrative.whatNext') }}
</p>
<!-- Choix -->
<ChoiceCards
v-if="choicePoint.choices?.length"
:choice-point="choicePoint"
/>
<!-- Si une seule option (contact) -->
<div
v-else-if="choicePoint.choices?.length === 1"
class="text-center"
>
<NuxtLink
:to="choicePoint.choices[0].destination"
class="inline-flex items-center gap-3 px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-xl hover:bg-sky-accent/90 transition-colors"
>
<span class="text-2xl">{{ choicePoint.choices[0].icon }}</span>
<span>{{ $i18n.locale === 'fr' ? choicePoint.choices[0].textFr : choicePoint.choices[0].textEn }}</span>
</NuxtLink>
</div>
</div>
</div>
</template>
```
### Schéma des chemins narratifs
```
┌─────────────┐
│ INTRO │
└──────┬──────┘
┌────────────┴────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ PROJETS │ │ COMPÉT. │
└────┬─────┘ └────┬─────┘
│ │
┌───────┴───────┐ ┌───────┴───────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│TÉMOIGN.│ │PARCOURS│ │TÉMOIGN.│ │PARCOURS│
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
▼ ▼ ▼ ▼
(suite) (suite) (suite) (suite)
│ │ │ │
└──────┬──────┴───────────┴──────┬──────┘
│ │
▼ ▼
┌──────────────────────────────────────┐
│ CONTACT │
│ (tous les chemins y mènent) │
└──────────────────────────────────────┘
```
### Dépendances
**Cette story nécessite :**
- Story 4.1 : ChoiceCards
- Story 4.2 : Intro narrative (premier choix)
- Story 3.5 : Store de progression (choices)
**Cette story prépare pour :**
- Story 4.7 : Révélation (fin des chemins)
- Story 4.8 : Page contact (destination finale)
### Project Structure Notes
**Fichiers à créer :**
```
frontend/app/
├── data/
│ └── narrativePaths.ts # CRÉER
├── composables/
│ └── useNarrativePath.ts # CRÉER
└── components/feature/
└── ZoneEndChoice.vue # CRÉER
```
**Fichiers à modifier :**
```
frontend/app/pages/projets.vue # AJOUTER ZoneEndChoice
frontend/app/pages/competences.vue # AJOUTER ZoneEndChoice
frontend/app/pages/temoignages.vue # AJOUTER ZoneEndChoice
frontend/app/pages/parcours.vue # AJOUTER ZoneEndChoice
```
### References
- [Source: docs/planning-artifacts/epics.md#Story-4.3]
- [Source: docs/planning-artifacts/ux-design-specification.md#Narrative-Paths]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Parcours-Narratifs]
### Technical Requirements
| Requirement | Value | Source |
|-------------|-------|--------|
| Points de choix | 2-3 | Epics |
| Parcours possibles | 4-8 | Epics |
| Toutes zones visitables | Oui | Epics |
| Tous chemins → contact | Oui | 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