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>
175 lines
4.0 KiB
TypeScript
175 lines
4.0 KiB
TypeScript
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,
|
|
}
|
|
}
|