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>
This commit is contained in:
2026-02-08 13:35:12 +01:00
parent 64b1a33d10
commit 7e87a341a2
38 changed files with 3037 additions and 96 deletions

View File

@@ -0,0 +1,174 @@
import type { EasterEggMeta } from './useFetchEasterEggs'
interface UseEasterEggDetectionOptions {
onFound: (slug: string) => void
}
// Konami Code
const KONAMI_CODE = [
'ArrowUp',
'ArrowUp',
'ArrowDown',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'ArrowLeft',
'ArrowRight',
'KeyB',
'KeyA',
]
export function useEasterEggDetection(options: UseEasterEggDetectionOptions) {
const { fetchList } = useFetchEasterEggs()
const progressionStore = useProgressionStore()
// State
const konamiIndex = ref(0)
const clickSequence = ref<string[]>([])
let konamiListenerAdded = false
// Load easter eggs on mount
onMounted(() => {
fetchList()
initKonamiListener()
})
// === Konami Code ===
function initKonamiListener() {
if (import.meta.client && !konamiListenerAdded) {
window.addEventListener('keydown', handleKonamiKey)
konamiListenerAdded = true
}
}
function handleKonamiKey(e: KeyboardEvent) {
if (e.code === KONAMI_CODE[konamiIndex.value]) {
konamiIndex.value++
if (konamiIndex.value === KONAMI_CODE.length) {
triggerEasterEgg('konami-master')
konamiIndex.value = 0
}
} else {
konamiIndex.value = 0
}
}
// === Click Detection ===
function detectClick(targetSlug: string, requiredClicks: number = 1) {
const clicks = ref(0)
function handleClick() {
clicks.value++
if (clicks.value >= requiredClicks) {
triggerEasterEgg(targetSlug)
clicks.value = 0
}
}
return { handleClick, clicks }
}
// === Hover Detection ===
function detectHover(targetSlug: string, hoverTime: number = 2000) {
let timeoutId: ReturnType<typeof setTimeout> | null = null
function handleMouseEnter() {
timeoutId = setTimeout(() => {
triggerEasterEgg(targetSlug)
}, hoverTime)
}
function handleMouseLeave() {
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
}
return { handleMouseEnter, handleMouseLeave }
}
// === Scroll Detection ===
function detectScrollBottom(targetSlug: string) {
let triggered = false
function checkScroll() {
if (triggered) return
const scrollTop = window.scrollY
const windowHeight = window.innerHeight
const docHeight = document.documentElement.scrollHeight
if (scrollTop + windowHeight >= docHeight - 50) {
triggerEasterEgg(targetSlug)
triggered = true
}
}
onMounted(() => {
if (import.meta.client) {
window.addEventListener('scroll', checkScroll, { passive: true })
}
})
onUnmounted(() => {
if (import.meta.client) {
window.removeEventListener('scroll', checkScroll)
}
})
}
// === Sequence Detection ===
function detectSequence(expectedSequence: string[], targetSlug: string) {
function addToSequence(item: string) {
clickSequence.value.push(item)
// Keep only the last N items
if (clickSequence.value.length > expectedSequence.length) {
clickSequence.value.shift()
}
// Check if sequence matches
if (clickSequence.value.length === expectedSequence.length) {
const match = clickSequence.value.every(
(val, idx) => val === expectedSequence[idx]
)
if (match) {
triggerEasterEgg(targetSlug)
clickSequence.value = []
}
}
}
return { addToSequence }
}
// === Trigger Easter Egg ===
function triggerEasterEgg(slug: string) {
// Check if already found
if (progressionStore.easterEggsFound.includes(slug)) {
return
}
// Mark as found
progressionStore.markEasterEggFound(slug)
// Notify
options.onFound(slug)
}
onUnmounted(() => {
if (import.meta.client && konamiListenerAdded) {
window.removeEventListener('keydown', handleKonamiKey)
konamiListenerAdded = false
}
})
return {
detectClick,
detectHover,
detectScrollBottom,
detectSequence,
triggerEasterEgg,
}
}