Epic 4: Chemins Narratifs, Challenge & Contact Stories implementees: - 4.1: Composant ChoiceCards pour choix narratifs binaires - 4.2: Sequence d'intro narrative avec Le Bug - 4.3: Chemins narratifs differencies avec useNarrativePath - 4.4: Table easter_eggs et systeme de detection (API + composable) - 4.5: Easter eggs UI (popup, notification, collection) - 4.6: Page challenge avec puzzle de code - 4.7: Page revelation "Monde de Code" - 4.8: Page contact avec formulaire et stats Fichiers crees: - Frontend: ChoiceCards, IntroSequence, ZoneEndChoice, EasterEggPopup, CodePuzzle, ChallengeSuccess, CodeWorld, et pages intro/challenge/revelation - API: EasterEggController, Model, Migration, Seeder Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { ZONES, NARRATIVE_PATHS, choiceIdToZone, type ZoneKey } from '~/data/narrativePaths'
|
|
import type { Choice, ChoicePoint } from '~/types/choice'
|
|
|
|
export function useNarrativePath() {
|
|
const progressionStore = useProgressionStore()
|
|
const { locale } = useI18n()
|
|
const route = useRoute()
|
|
|
|
// Déterminer le chemin actuel basé sur les choix
|
|
const currentPath = computed<ZoneKey[] | null>(() => {
|
|
const choices = progressionStore.choices
|
|
|
|
// Premier choix (intro)
|
|
const introChoice = choices['intro_first_choice']
|
|
if (!introChoice) return null
|
|
|
|
const startZone = choiceIdToZone(introChoice)
|
|
if (!startZone) return null
|
|
|
|
// Filtrer les chemins qui commencent par cette zone
|
|
let possiblePaths = NARRATIVE_PATHS.filter(path => path[0] === startZone)
|
|
|
|
// Affiner avec les choix suivants si disponibles
|
|
const afterFirstZoneChoice = choices[`after_${startZone}`]
|
|
if (afterFirstZoneChoice && possiblePaths.length > 1) {
|
|
const secondZone = choiceIdToZone(afterFirstZoneChoice)
|
|
if (secondZone) {
|
|
possiblePaths = possiblePaths.filter(path => path[1] === secondZone)
|
|
}
|
|
}
|
|
|
|
return possiblePaths[0] || null
|
|
})
|
|
|
|
// Zone actuelle basée sur la route
|
|
const currentZone = computed<ZoneKey | null>(() => {
|
|
const path = route.path.toLowerCase()
|
|
|
|
if (path.includes('projets') || path.includes('projects')) return 'projects'
|
|
if (path.includes('competences') || path.includes('skills')) return 'skills'
|
|
if (path.includes('temoignages') || path.includes('testimonials')) return 'testimonials'
|
|
if (path.includes('parcours') || path.includes('journey')) return 'journey'
|
|
if (path.includes('contact')) return 'contact'
|
|
|
|
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<ZoneKey | null>(() => {
|
|
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 (excluant contact)
|
|
const remainingZones = computed<ZoneKey[]>(() => {
|
|
const mainZones: ZoneKey[] = ['projects', 'skills', 'testimonials', 'journey']
|
|
const visited = progressionStore.visitedSections
|
|
|
|
return mainZones.filter(zone => !visited.includes(zone))
|
|
})
|
|
|
|
// Obtenir la route pour une zone
|
|
function getZoneRoute(zone: ZoneKey): string {
|
|
const zoneInfo = ZONES[zone]
|
|
if (!zoneInfo) return '/'
|
|
return locale.value === 'fr' ? zoneInfo.routeFr : zoneInfo.routeEn
|
|
}
|
|
|
|
// Générer le ChoicePoint pour après la zone actuelle
|
|
function getNextChoicePoint(): ChoicePoint | null {
|
|
if (remainingZones.value.length === 0) {
|
|
// Plus de zones, proposer le contact uniquement
|
|
return {
|
|
id: 'go_to_contact',
|
|
questionFr: 'Tu as tout exploré ! Prêt à me rencontrer ?',
|
|
questionEn: 'You explored everything! Ready to meet me?',
|
|
choices: [
|
|
createChoice('contact'),
|
|
createChoice('contact'), // Dupliqué car ChoicePoint attend 2 choices
|
|
],
|
|
context: 'contact_ready',
|
|
}
|
|
}
|
|
|
|
// Proposer les 2 prochaines zones non visitées
|
|
const nextTwo = remainingZones.value.slice(0, 2)
|
|
|
|
if (nextTwo.length === 1) {
|
|
// Une seule zone restante, proposer zone + contact
|
|
return {
|
|
id: `after_${currentZone.value}`,
|
|
questionFr: 'Où vas-tu ensuite ?',
|
|
questionEn: 'Where to next?',
|
|
choices: [
|
|
createChoice(nextTwo[0]),
|
|
createChoice('contact'),
|
|
],
|
|
context: 'one_zone_left',
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: `after_${currentZone.value}`,
|
|
questionFr: 'Où vas-tu ensuite ?',
|
|
questionEn: 'Where to next?',
|
|
choices: [
|
|
createChoice(nextTwo[0]),
|
|
createChoice(nextTwo[1]),
|
|
],
|
|
context: 'zone_choice',
|
|
}
|
|
}
|
|
|
|
// Créer un objet Choice pour une zone
|
|
function createChoice(zone: ZoneKey): Choice {
|
|
const zoneInfo = ZONES[zone]
|
|
return {
|
|
id: `choice_${zone}`,
|
|
textFr: zoneInfo.labelFr,
|
|
textEn: zoneInfo.labelEn,
|
|
icon: zoneInfo.icon,
|
|
destination: getZoneRoute(zone),
|
|
zoneColor: zoneInfo.color,
|
|
}
|
|
}
|
|
|
|
// Vérifier si le contact est débloqué
|
|
const isContactReady = computed(() => {
|
|
return remainingZones.value.length === 0 || progressionStore.isContactUnlocked
|
|
})
|
|
|
|
return {
|
|
currentPath,
|
|
currentZone,
|
|
currentZoneIndex,
|
|
suggestedNextZone,
|
|
remainingZones,
|
|
isContactReady,
|
|
getZoneRoute,
|
|
getNextChoicePoint,
|
|
createChoice,
|
|
}
|
|
}
|