Files
Portfolio-Game/frontend/app/composables/useNarrator.ts
skycel 99fa61fcaa 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>
2026-02-07 03:04:07 +01:00

120 lines
3.0 KiB
TypeScript

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,
}
}