✨ feat(frontend): système narrateur contextuel avec arc de révélation
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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Story 3.3: Textes narrateur contextuels et arc de révélation
|
# Story 3.3: Textes narrateur contextuels et arc de révélation
|
||||||
|
|
||||||
Status: ready-for-dev
|
Status: review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
@@ -22,63 +22,63 @@ so that l'expérience est personnalisée et le narrateur devient familier.
|
|||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [ ] **Task 1: Créer le composable useNarrator** (AC: #1, #2, #3, #4, #5, #6)
|
- [x] **Task 1: Créer le composable useNarrator** (AC: #1, #2, #3, #4, #5, #6)
|
||||||
- [ ] Créer `frontend/app/composables/useNarrator.ts`
|
- [x] Créer `frontend/app/composables/useNarrator.ts`
|
||||||
- [ ] Centraliser la logique d'affichage du narrateur
|
- [x] Centraliser la logique d'affichage du narrateur
|
||||||
- [ ] Exposer les méthodes : showIntro, showTransition, showEncouragement, showHint, showWelcomeBack, showContactUnlocked
|
- [x] Exposer les méthodes : showIntro, showTransition, showEncouragement, showHint, showWelcomeBack, showContactUnlocked
|
||||||
- [ ] Gérer la queue de messages (ne pas interrompre un message en cours)
|
- [x] Gérer la queue de messages (ne pas interrompre un message en cours)
|
||||||
- [ ] Intégrer le composable useFetchNarratorText
|
- [x] Intégrer le composable useFetchNarratorText
|
||||||
|
|
||||||
- [ ] **Task 2: Implémenter les déclencheurs de transition** (AC: #2)
|
- [x] **Task 2: Implémenter les déclencheurs de transition** (AC: #2)
|
||||||
- [ ] Déclencher sur navigation vers /projets (transition_projects)
|
- [x] Déclencher sur navigation vers /projets (transition_projects)
|
||||||
- [ ] Déclencher sur navigation vers /competences (transition_skills)
|
- [x] Déclencher sur navigation vers /competences (transition_skills)
|
||||||
- [ ] Déclencher sur navigation vers /temoignages (transition_testimonials)
|
- [x] Déclencher sur navigation vers /temoignages (transition_testimonials)
|
||||||
- [ ] Déclencher sur navigation vers /parcours (transition_journey)
|
- [x] Déclencher sur navigation vers /parcours (transition_journey)
|
||||||
- [ ] Utiliser un plugin Nuxt ou watcher sur la route
|
- [x] Utiliser un plugin Nuxt ou watcher sur la route
|
||||||
|
|
||||||
- [ ] **Task 3: Implémenter la détection d'inactivité** (AC: #4)
|
- [x] **Task 3: Implémenter la détection d'inactivité** (AC: #4)
|
||||||
- [ ] Créer `frontend/app/composables/useIdleDetection.ts`
|
- [x] Créer `frontend/app/composables/useIdleDetection.ts`
|
||||||
- [ ] Détecter l'absence d'interaction > 30 secondes
|
- [x] Détecter l'absence d'interaction > 30 secondes
|
||||||
- [ ] Écouter mouse, keyboard, touch, scroll
|
- [x] Écouter mouse, keyboard, touch, scroll
|
||||||
- [ ] Déclencher `showHint()` quand idle détecté
|
- [x] Déclencher `showHint()` quand idle détecté
|
||||||
- [ ] Ne pas répéter les hints trop souvent (cooldown de 2min)
|
- [x] Ne pas répéter les hints trop souvent (cooldown de 2min)
|
||||||
|
|
||||||
- [ ] **Task 4: Implémenter les encouragements basés sur la progression** (AC: #3)
|
- [x] **Task 4: Implémenter les encouragements basés sur la progression** (AC: #3)
|
||||||
- [ ] Watcher sur `completionPercent` du store
|
- [x] Watcher sur `completionPercent` du store
|
||||||
- [ ] Déclencher à 25%, 50%, 75%
|
- [x] Déclencher à 25%, 50%, 75%
|
||||||
- [ ] Garder en mémoire les seuils déjà atteints (ne pas répéter)
|
- [x] Garder en mémoire les seuils déjà atteints (ne pas répéter)
|
||||||
|
|
||||||
- [ ] **Task 5: Implémenter l'arc de révélation du Bug** (AC: #7, #8, #9)
|
- [x] **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 :
|
- [x] Définir les seuils de progression pour chaque stage :
|
||||||
- Stage 1 : 0-19%
|
- Stage 1 : 0-19%
|
||||||
- Stage 2 : 20-39%
|
- Stage 2 : 20-39%
|
||||||
- Stage 3 : 40-59%
|
- Stage 3 : 40-59%
|
||||||
- Stage 4 : 60-79%
|
- Stage 4 : 60-79%
|
||||||
- Stage 5 : 80-100%
|
- Stage 5 : 80-100%
|
||||||
- [ ] Mettre à jour `narratorStage` dans le store
|
- [x] Mettre à jour `narratorStage` dans le store (getter calculé)
|
||||||
- [ ] L'image du Bug se met à jour automatiquement via NarratorBubble
|
- [x] L'image du Bug se met à jour automatiquement via NarratorBubble
|
||||||
|
|
||||||
- [ ] **Task 6: Implémenter le message "Bienvenue à nouveau"** (AC: #5)
|
- [x] **Task 6: Implémenter le message "Bienvenue à nouveau"** (AC: #5)
|
||||||
- [ ] Détecter au chargement si `visitedSections` n'est pas vide (progression existante)
|
- [x] Détecter au chargement si `visitedSections` n'est pas vide (progression existante)
|
||||||
- [ ] Afficher le message `welcome_back` dans ce cas
|
- [x] Afficher le message `welcome_back` dans ce cas
|
||||||
- [ ] Sinon afficher le message `intro` normal
|
- [x] Sinon afficher le message `intro` normal
|
||||||
|
|
||||||
- [ ] **Task 7: Implémenter le message de déblocage contact** (AC: #6)
|
- [x] **Task 7: Implémenter le message de déblocage contact** (AC: #6)
|
||||||
- [ ] Watcher sur `contactUnlocked` du store
|
- [x] Watcher sur `contactUnlocked` du store
|
||||||
- [ ] Quand passe à `true`, afficher `contact_unlocked`
|
- [x] Quand passe à `true`, afficher `contact_unlocked`
|
||||||
|
|
||||||
- [ ] **Task 8: Intégrer dans le layout principal**
|
- [x] **Task 8: Intégrer dans le layout principal**
|
||||||
- [ ] Ajouter le NarratorBubble dans default.vue ou adventure.vue
|
- [x] Ajouter le NarratorBubble dans adventure.vue
|
||||||
- [ ] Initialiser useNarrator dans le layout
|
- [x] Initialiser useNarrator dans le layout
|
||||||
- [ ] Gérer l'état visible/hidden du narrateur
|
- [x] Gérer l'état visible/hidden du narrateur
|
||||||
|
|
||||||
- [ ] **Task 9: Tests et validation**
|
- [x] **Task 9: Tests et validation**
|
||||||
- [ ] Tester le message d'accueil adapté au héros
|
- [x] Tester le message d'accueil adapté au héros
|
||||||
- [ ] Tester les transitions entre pages
|
- [x] Tester les transitions entre pages
|
||||||
- [ ] Vérifier les encouragements à 25/50/75%
|
- [x] Vérifier les encouragements à 25/50/75%
|
||||||
- [ ] Tester la détection d'inactivité
|
- [x] Tester la détection d'inactivité
|
||||||
- [ ] Valider l'évolution du Bug (5 stages)
|
- [x] Valider l'évolution du Bug (5 stages)
|
||||||
- [ ] Tester le "Bienvenue à nouveau"
|
- [x] Tester le "Bienvenue à nouveau"
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ development_status:
|
|||||||
epic-3: in-progress
|
epic-3: in-progress
|
||||||
3-1-table-narrator-texts-api-narrateur: review
|
3-1-table-narrator-texts-api-narrateur: review
|
||||||
3-2-composant-narratorbubble-le-bug: review
|
3-2-composant-narratorbubble-le-bug: review
|
||||||
3-3-textes-narrateur-contextuels-arc-revelation: ready-for-dev
|
3-3-textes-narrateur-contextuels-arc-revelation: review
|
||||||
3-4-barre-progression-globale-xp-bar: ready-for-dev
|
3-4-barre-progression-globale-xp-bar: ready-for-dev
|
||||||
3-5-logique-progression-deblocage-contact: ready-for-dev
|
3-5-logique-progression-deblocage-contact: ready-for-dev
|
||||||
3-6-carte-interactive-desktop-konvajs: ready-for-dev
|
3-6-carte-interactive-desktop-konvajs: ready-for-dev
|
||||||
|
|||||||
44
frontend/app/composables/useIdleDetection.ts
Normal file
44
frontend/app/composables/useIdleDetection.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export interface UseIdleDetectionOptions {
|
||||||
|
timeout?: number
|
||||||
|
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']
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
onMounted(() => {
|
||||||
|
events.forEach((event) => {
|
||||||
|
window.addEventListener(event, resetTimer, { passive: true })
|
||||||
|
})
|
||||||
|
resetTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
events.forEach((event) => {
|
||||||
|
window.removeEventListener(event, resetTimer)
|
||||||
|
})
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isIdle: readonly(isIdle) }
|
||||||
|
}
|
||||||
119
frontend/app/composables/useNarrator.ts
Normal file
119
frontend/app/composables/useNarrator.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import type { NarratorContext, HeroType } from './useFetchNarratorText'
|
||||||
|
|
||||||
|
interface NarratorMessage {
|
||||||
|
context: NarratorContext
|
||||||
|
priority: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const HINT_COOLDOWN = 120000
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const currentMessage = ref('')
|
||||||
|
const messageQueue = ref<NarratorMessage[]>([])
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const shownEncouragements = ref<Set<number>>(new Set())
|
||||||
|
const lastHintTime = ref(0)
|
||||||
|
|
||||||
|
export function useNarrator() {
|
||||||
|
const { fetchText } = useFetchNarratorText()
|
||||||
|
const progressionStore = useProgressionStore()
|
||||||
|
|
||||||
|
async function queueMessage(context: NarratorContext, 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 heroType = progressionStore.hero as HeroType | undefined
|
||||||
|
const response = await fetchText(next.context, heroType ?? undefined)
|
||||||
|
if (response) {
|
||||||
|
currentMessage.value = response.text
|
||||||
|
isVisible.value = true
|
||||||
|
} else {
|
||||||
|
processQueue()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
processQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
isVisible.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
processQueue()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showIntro() {
|
||||||
|
if (progressionStore.hasExistingProgress) {
|
||||||
|
await queueMessage('welcome_back', 10)
|
||||||
|
} else {
|
||||||
|
await queueMessage('intro', 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showTransition(zone: 'projects' | 'skills' | 'testimonials' | 'journey') {
|
||||||
|
const contextMap: Record<string, NarratorContext> = {
|
||||||
|
projects: 'transition_projects',
|
||||||
|
skills: 'transition_skills',
|
||||||
|
testimonials: 'transition_testimonials',
|
||||||
|
journey: 'transition_journey',
|
||||||
|
}
|
||||||
|
await queueMessage(contextMap[zone], 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showEncouragement(percent: number) {
|
||||||
|
let context: NarratorContext | 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisible: readonly(isVisible),
|
||||||
|
currentMessage: readonly(currentMessage),
|
||||||
|
hide,
|
||||||
|
showIntro,
|
||||||
|
showTransition,
|
||||||
|
showEncouragement,
|
||||||
|
showHint,
|
||||||
|
showContactUnlocked,
|
||||||
|
}
|
||||||
|
}
|
||||||
40
frontend/app/layouts/adventure.vue
Normal file
40
frontend/app/layouts/adventure.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const narrator = useNarrator()
|
||||||
|
|
||||||
|
useIdleDetection({
|
||||||
|
timeout: 30000,
|
||||||
|
onIdle: () => {
|
||||||
|
narrator.showHint()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
narrator.showIntro()
|
||||||
|
}, 1500)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-sky-dark text-sky-text flex flex-col">
|
||||||
|
<LayoutAppHeader />
|
||||||
|
|
||||||
|
<main class="flex-1">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<LayoutAppFooter />
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<LayoutConsentBanner />
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<NarratorBubble
|
||||||
|
:message="narrator.currentMessage.value"
|
||||||
|
:visible="narrator.isVisible.value"
|
||||||
|
@close="narrator.hide()"
|
||||||
|
/>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -80,6 +80,10 @@
|
|||||||
import type { Skill } from '~/types/skill'
|
import type { Skill } from '~/types/skill'
|
||||||
import { useProgressionStore } from '~/stores/progression'
|
import { useProgressionStore } from '~/stores/progression'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'adventure',
|
||||||
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { setPageMeta } = useSeo()
|
const { setPageMeta } = useSeo()
|
||||||
const store = useProgressionStore()
|
const store = useProgressionStore()
|
||||||
|
|||||||
@@ -49,6 +49,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Milestone } from '~/components/feature/TimelineItem.vue'
|
import type { Milestone } from '~/components/feature/TimelineItem.vue'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'adventure',
|
||||||
|
})
|
||||||
|
|
||||||
const { setPageMeta } = useSeo()
|
const { setPageMeta } = useSeo()
|
||||||
const { t, tm } = useI18n()
|
const { t, tm } = useI18n()
|
||||||
const progressStore = useProgressStore()
|
const progressStore = useProgressStore()
|
||||||
|
|||||||
@@ -161,6 +161,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useProgressionStore } from '~/stores/progression'
|
import { useProgressionStore } from '~/stores/progression'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'adventure',
|
||||||
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
|
|||||||
@@ -54,6 +54,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useProgressionStore } from '~/stores/progression'
|
import { useProgressionStore } from '~/stores/progression'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'adventure',
|
||||||
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { setPageMeta } = useSeo()
|
const { setPageMeta } = useSeo()
|
||||||
const store = useProgressionStore()
|
const store = useProgressionStore()
|
||||||
|
|||||||
@@ -119,6 +119,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Testimonial } from '~/types/testimonial'
|
import type { Testimonial } from '~/types/testimonial'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'adventure',
|
||||||
|
})
|
||||||
|
|
||||||
const { setPageMeta } = useSeo()
|
const { setPageMeta } = useSeo()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const progressStore = useProgressStore()
|
const progressStore = useProgressStore()
|
||||||
|
|||||||
42
frontend/app/plugins/narrator-transitions.client.ts
Normal file
42
frontend/app/plugins/narrator-transitions.client.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const narrator = useNarrator()
|
||||||
|
const router = useRouter()
|
||||||
|
const progressionStore = useProgressionStore()
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcedSections = new Set<string>()
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
const zone = routeContextMap[to.path]
|
||||||
|
if (zone && !announcedSections.has(zone)) {
|
||||||
|
announcedSections.add(zone)
|
||||||
|
narrator.showTransition(zone)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => progressionStore.completionPercent,
|
||||||
|
(percent) => {
|
||||||
|
narrator.showEncouragement(percent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => progressionStore.contactUnlocked,
|
||||||
|
(unlocked, wasUnlocked) => {
|
||||||
|
if (unlocked && !wasUnlocked) {
|
||||||
|
narrator.showContactUnlocked()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -11,11 +11,21 @@ export interface ProgressionState {
|
|||||||
easterEggsFound: string[]
|
easterEggsFound: string[]
|
||||||
challengeCompleted: boolean
|
challengeCompleted: boolean
|
||||||
contactUnlocked: boolean
|
contactUnlocked: boolean
|
||||||
narratorStage: number
|
|
||||||
choices: Record<string, string>
|
choices: Record<string, string>
|
||||||
consentGiven: boolean | null
|
consentGiven: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NARRATOR_STAGE_THRESHOLDS = [0, 20, 40, 60, 80]
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
const SECTIONS = ['projets', 'competences', 'temoignages', 'parcours'] as const
|
const SECTIONS = ['projets', 'competences', 'temoignages', 'parcours'] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +79,6 @@ export const useProgressionStore = defineStore('progression', {
|
|||||||
easterEggsFound: [],
|
easterEggsFound: [],
|
||||||
challengeCompleted: false,
|
challengeCompleted: false,
|
||||||
contactUnlocked: false,
|
contactUnlocked: false,
|
||||||
narratorStage: 1,
|
|
||||||
choices: {},
|
choices: {},
|
||||||
consentGiven: null,
|
consentGiven: null,
|
||||||
}),
|
}),
|
||||||
@@ -82,6 +91,8 @@ export const useProgressionStore = defineStore('progression', {
|
|||||||
progressPercent: (state) => Math.round((state.visitedSections.length / SECTIONS.length) * 100),
|
progressPercent: (state) => Math.round((state.visitedSections.length / SECTIONS.length) * 100),
|
||||||
|
|
||||||
hasExistingProgress: (state) => state.visitedSections.length > 0 || state.hero !== null,
|
hasExistingProgress: (state) => state.visitedSections.length > 0 || state.hero !== null,
|
||||||
|
|
||||||
|
narratorStage: (state) => calculateNarratorStage(state.completionPercent),
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -121,12 +132,6 @@ export const useProgressionStore = defineStore('progression', {
|
|||||||
this.contactUnlocked = true
|
this.contactUnlocked = true
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNarratorStage(stage: number) {
|
|
||||||
if (stage >= 1 && stage <= 5) {
|
|
||||||
this.narratorStage = stage
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
makeChoice(choiceId: string, value: string) {
|
makeChoice(choiceId: string, value: string) {
|
||||||
this.choices[choiceId] = value
|
this.choices[choiceId] = value
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user