Files
Portfolio-Game/frontend/app/stores/progression.ts
skycel 7e87a341a2 feat(epic-4): chemins narratifs, easter eggs, challenge et contact
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>
2026-02-08 13:35:12 +01:00

167 lines
4.0 KiB
TypeScript

import { defineStore } from 'pinia'
export type HeroType = 'recruteur' | 'client' | 'dev'
export interface ProgressionState {
sessionId: string
hero: HeroType | null
currentPath: string
visitedSections: string[]
completionPercent: number
easterEggsFound: string[]
challengeCompleted: boolean
contactUnlocked: boolean
choices: Record<string, string>
consentGiven: boolean | null
introSeen: boolean
}
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
/**
* Storage conditionnel : ne persiste que si consentGiven === true.
* La lecture est toujours autorisée (pour restaurer une session existante).
*/
const conditionalStorage: Storage = {
get length() {
return import.meta.client ? localStorage.length : 0
},
key(index: number) {
return import.meta.client ? localStorage.key(index) : null
},
getItem(key: string) {
if (import.meta.client) {
return localStorage.getItem(key)
}
return null
},
setItem(key: string, value: string) {
if (import.meta.client) {
try {
const parsed = JSON.parse(value)
if (parsed.consentGiven === true) {
localStorage.setItem(key, value)
}
} catch {
// Si parsing échoue, ne pas persister
}
}
},
removeItem(key: string) {
if (import.meta.client) {
localStorage.removeItem(key)
}
},
clear() {
if (import.meta.client) {
localStorage.clear()
}
},
}
export const useProgressionStore = defineStore('progression', {
state: (): ProgressionState => ({
sessionId: '',
hero: null,
currentPath: 'start',
visitedSections: [],
completionPercent: 0,
easterEggsFound: [],
challengeCompleted: false,
contactUnlocked: false,
choices: {},
consentGiven: null,
introSeen: false,
}),
getters: {
hasVisited: (state) => (section: string) => state.visitedSections.includes(section),
isContactUnlocked: (state) => state.visitedSections.length >= 2 || state.contactUnlocked,
progressPercent: (state) => Math.round((state.visitedSections.length / SECTIONS.length) * 100),
hasExistingProgress: (state) => state.visitedSections.length > 0 || state.hero !== null,
narratorStage: (state) => calculateNarratorStage(state.completionPercent),
easterEggsFoundCount: (state) => state.easterEggsFound.length,
},
actions: {
initSession() {
if (!this.sessionId && import.meta.client) {
this.sessionId = crypto.randomUUID()
}
},
setHero(hero: HeroType) {
this.hero = hero
this.initSession()
},
visitSection(section: string) {
if (!this.visitedSections.includes(section)) {
this.visitedSections.push(section)
this.completionPercent = this.progressPercent
if (this.visitedSections.length >= 2) {
this.contactUnlocked = true
}
}
},
findEasterEgg(slug: string) {
if (!this.easterEggsFound.includes(slug)) {
this.easterEggsFound.push(slug)
}
},
markEasterEggFound(slug: string) {
this.findEasterEgg(slug)
},
completeChallenge() {
this.challengeCompleted = true
},
unlockContact() {
this.contactUnlocked = true
},
makeChoice(choiceId: string, value: string) {
this.choices[choiceId] = value
},
setIntroSeen(seen: boolean) {
this.introSeen = seen
},
setConsent(given: boolean) {
this.consentGiven = given
if (given) {
this.initSession()
} else if (import.meta.client) {
// Si refus, supprimer les données stockées
localStorage.removeItem('skycel-progression')
}
},
},
persist: {
key: 'skycel-progression',
storage: conditionalStorage,
},
})