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>
120 lines
3.0 KiB
TypeScript
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,
|
|
}
|
|
}
|