// ✨ GENIE SYSTEM JAVASCRIPT ANIMATIONS ✨
class GenieAnimations {
constructor() {
this.isAnimating = false;
this.particles = [];
this.genieState = 'normal'; // normal, talking, laughing
this.currentGenieImage = null;
this.dailySummonLimit = 3;
this.userStuckThreshold = 300000; // 5 minutes of inactivity
this.lastActivityTime = Date.now();
this.init();
}
init() {
// Enhance existing animations with JavaScript
this.setupDynamicEffects();
this.setupParticleSystem();
this.preloadGenieImages();
this.setupActivityTracking();
this.setupDailySummonTracking();
console.log('🎭 Genie animations initialized');
}
// Track daily summons (3 questions per day limit)
setupDailySummonTracking() {
const today = new Date().toDateString();
const storedData = localStorage.getItem('genie_summons');
if (storedData) {
const data = JSON.parse(storedData);
if (data.date === today) {
this.todaySummons = data.count;
} else {
// New day, reset count
this.todaySummons = 0;
this.saveDailySummons();
}
} else {
this.todaySummons = 0;
this.saveDailySummons();
}
}
saveDailySummons() {
const today = new Date().toDateString();
localStorage.setItem('genie_summons', JSON.stringify({
date: today,
count: this.todaySummons
}));
}
// Check if user can summon genie
canSummonGenie() {
return this.todaySummons < this.dailySummonLimit;
}
// Use one summon
useSummon() {
if (this.canSummonGenie()) {
this.todaySummons++;
this.saveDailySummons();
return true;
}
return false;
}
// Track user activity to detect when stuck
setupActivityTracking() {
// Track various user interactions
const activities = ['click', 'keypress', 'scroll', 'touchstart'];
activities.forEach(event => {
document.addEventListener(event, () => {
this.lastActivityTime = Date.now();
});
});
// Check periodically if user seems stuck
setInterval(() => {
this.checkIfUserStuck();
}, 60000); // Check every minute
}
// Detect if user appears to be stuck
checkIfUserStuck() {
const timeSinceActivity = Date.now() - this.lastActivityTime;
if (timeSinceActivity > this.userStuckThreshold && this.canSummonGenie()) {
// User seems stuck and can still summon genie
this.offerGenieHelp();
}
}
// Offer subtle genie help when user is stuck
offerGenieHelp() {
// Don't offer if already showing genie
if (document.querySelector('.genie-appearance')) return;
const helpOffer = document.createElement('div');
helpOffer.className = 'genie-help-offer';
helpOffer.style.cssText = `
position: fixed;
bottom: 80px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: #ffd700;
padding: 15px;
border-radius: 10px;
border: 2px solid #667aea;
font-size: 0.9em;
z-index: 9999;
opacity: 0;
transition: opacity 0.5s ease;
cursor: pointer;
max-width: 250px;
`;
helpOffer.innerHTML = `
✨ Need guidance? ✨
The genie senses your struggle...
(${this.dailySummonLimit - this.todaySummons} questions left today)
`;
helpOffer.addEventListener('click', () => {
helpOffer.remove();
this.summonGenieForHelp();
});
document.body.appendChild(helpOffer);
// Fade in
setTimeout(() => {
helpOffer.style.opacity = '1';
}, 100);
// Auto-remove after 10 seconds
setTimeout(() => {
if (helpOffer.parentNode) {
helpOffer.style.opacity = '0';
setTimeout(() => helpOffer.remove(), 500);
}
}, 10000);
}
// Summon genie specifically for help when stuck
summonGenieForHelp() {
if (!this.useSummon()) {
this.showDailyLimitMessage();
return;
}
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
text-align: center;
`;
document.body.appendChild(container);
const stuckMessages = [
'Path unclear?',
'Seek patience.',
'Try again.',
'Look closer.',
'Different angle.'
];
const response = stuckMessages[Math.floor(Math.random() * stuckMessages.length)];
this.showGenieWisdom(container, response);
// Auto-remove after showing message
setTimeout(() => {
container.remove();
}, 5000);
}
// Preload all genie images for smooth transitions
preloadGenieImages() {
this.genieImages = {
normal: new Image(),
laughing: new Image(),
lamp: new Image()
};
this.genieImages.normal.src = 'GENIE.jpg';
this.genieImages.laughing.src = 'GENIE-LAUGH.jpg';
this.genieImages.lamp.src = 'LAMP.jpg';
// Ensure images are loaded
this.genieImages.normal.onload = () => console.log('✨ Normal genie image loaded');
this.genieImages.laughing.onload = () => console.log('😄 Laughing genie image loaded');
this.genieImages.lamp.onload = () => console.log('🪔 Lamp image loaded');
}
// Create pixel-perfect genie image element
createGenieImage(state = 'normal', className = '') {
const img = document.createElement('img');
img.className = `genie-image ${className} ${state}`;
img.src = this.genieImages[state].src;
img.alt = `Genie ${state}`;
return img;
}
// Create interactive lamp image
createLampImage(className = '') {
const img = document.createElement('img');
img.className = `lamp-image ${className}`;
img.src = this.genieImages.lamp.src;
img.alt = 'Magic Lamp';
return img;
}
// Transition genie between states with smooth animation
transitionGenieState(genieElement, newState, duration = 300) {
if (!genieElement || !this.genieImages[newState]) return;
return new Promise((resolve) => {
// Fade out current image
genieElement.style.opacity = '0';
genieElement.style.transform = 'scale(0.9)';
setTimeout(() => {
// Change image source and class
genieElement.src = this.genieImages[newState].src;
genieElement.className = genieElement.className.replace(/\b(normal|talking|laughing)\b/g, newState);
// Fade in new image
genieElement.style.opacity = '1';
genieElement.style.transform = 'scale(1)';
this.genieState = newState;
this.currentGenieImage = genieElement;
resolve();
}, duration);
});
}
// Dynamic lamp glow that responds to user proximity
createProximityGlow(lampElement) {
if (!lampElement) return;
const glowIntensity = (distance) => {
const maxDistance = 100;
const intensity = Math.max(0, (maxDistance - distance) / maxDistance);
return intensity;
};
document.addEventListener('mousemove', (e) => {
const rect = lampElement.getBoundingClientRect();
const lampCenter = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
const distance = Math.sqrt(
Math.pow(e.clientX - lampCenter.x, 2) +
Math.pow(e.clientY - lampCenter.y, 2)
);
const intensity = glowIntensity(distance);
const glowSize = 20 + (intensity * 30);
const opacity = 0.3 + (intensity * 0.4);
lampElement.style.filter = `drop-shadow(0 0 ${glowSize}px rgba(255, 215, 0, ${opacity}))`;
lampElement.style.transform = `scale(${1 + intensity * 0.1})`;
});
}
// Enhanced lamp rubbing with pixel art image
createInteractiveLamp(container) {
const lampImg = this.createLampImage('interactive floating');
// Add rubbing interaction
let isRubbing = false;
let rubCount = 0;
const smokeContainer = document.createElement('div');
smokeContainer.className = 'rubbing-smoke-container';
smokeContainer.style.cssText = `
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
pointer-events: none;
z-index: 1;
`;
const lampContainer = document.createElement('div');
lampContainer.style.cssText = `
position: relative;
display: inline-block;
margin: 20px;
`;
lampContainer.appendChild(lampImg);
lampContainer.appendChild(smokeContainer);
// Mouse events for rubbing
const startRubbing = () => {
if (isRubbing) return;
isRubbing = true;
lampImg.classList.add('rubbing');
this.startRubbingSmoke(smokeContainer);
};
const stopRubbing = () => {
if (!isRubbing) return;
isRubbing = false;
lampImg.classList.remove('rubbing');
this.stopRubbingSmoke(smokeContainer);
rubCount++;
if (rubCount >= 3) {
this.triggerGenieAppearance(container);
rubCount = 0;
}
};
// Touch and mouse events
lampImg.addEventListener('mousedown', startRubbing);
lampImg.addEventListener('touchstart', startRubbing);
document.addEventListener('mouseup', stopRubbing);
document.addEventListener('touchend', stopRubbing);
container.appendChild(lampContainer);
return { lampContainer, lampImg };
}
// Animated smoke effect during lamp rubbing
startRubbingSmoke(smokeContainer) {
this.smokeInterval = setInterval(() => {
this.createSmokeParticle(smokeContainer);
}, 150);
}
stopRubbingSmoke(smokeContainer) {
clearInterval(this.smokeInterval);
setTimeout(() => {
smokeContainer.innerHTML = '';
}, 1000);
}
// Show daily limit reached message
showDailyLimitMessage() {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
`;
modal.innerHTML = `
🧞♂️
Genie Rests
The ancient spirit has granted you ${this.dailySummonLimit} questions today.
Return tomorrow to seek more wisdom.
`;
document.body.appendChild(modal);
}
// Trigger genie appearance after sufficient rubbing
triggerGenieAppearance(container) {
// Check if user has summons left
if (!this.canSummonGenie()) {
this.showDailyLimitMessage();
return;
}
// Use one summon
if (!this.useSummon()) {
this.showDailyLimitMessage();
return;
}
// 🎵 TRIGGER VISUAL MUSIC EXPERIENCE! 🎵
// Integrate with the amazing visual music system
this.triggerMysticalMusicExperience();
// Create dramatic emergence effect
this.createParticleBurst(container, {
count: 80,
colors: ['#667aea', '#ffd700', '#9f7aea', '#ff6b6b'],
duration: 3000,
speed: 8
});
// Show genie with typewriter effect and summon count
setTimeout(() => {
const remaining = this.dailySummonLimit - this.todaySummons;
const message = remaining > 0 ?
`Awakened. ${remaining} questions remain.` :
"Awakened. Final question.";
this.showGenieWithMessage(container, message);
}, 1500);
}
// 🎵 Integrate with Visual Music Experience System 🎵
triggerMysticalMusicExperience() {
// Check if the visual music system exists (created by other Claude)
if (typeof window.triggerVisualMusicExperience === 'function') {
// Trigger the floating music player with mystical/transcendental music
window.triggerVisualMusicExperience({
type: 'genie_summoning',
intensity: 'mystical',
visualStyle: 'cosmic_particles', // Use cosmic particles for genie
musicType: 'transcendental',
autoplay: false, // Let user choose to play
message: '🧞♂️ The genie awakens with mystical energies...'
});
} else if (typeof window.createHoveringMusicPlayer === 'function') {
// Fallback to hovering music player
window.createHoveringMusicPlayer({
achievement: 'Genie Summoned',
musicUrl: 'https://www.youtube.com/watch?v=t9-CS2v8wcc', // Mystical music
visualType: 'cosmic'
});
} else {
console.log('🎵 Visual music system not available, using audio-only effects');
this.playMysticalSoundEffect();
}
}
// Audio-only mystical sound effect (fallback)
playMysticalSoundEffect() {
try {
// Create subtle mystical audio context
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Mystical frequency (528 Hz - "Love frequency")
oscillator.frequency.setValueAtTime(528, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(432, audioContext.currentTime + 2);
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.5);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 2);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 2);
} catch (error) {
console.log('🔇 Audio context not available');
}
}
// Show genie with pixel art and message
showGenieWithMessage(container, message, isWisdom = false) {
const genieContainer = document.createElement('div');
genieContainer.className = 'genie-appearance';
genieContainer.style.cssText = `
text-align: center;
margin: 30px 0;
opacity: 0;
transform: scale(0.8);
transition: all 0.8s ease;
`;
// Choose genie state based on message type
const state = isWisdom ? 'normal' : 'laughing';
const genieImg = this.createGenieImage(state, 'large floating');
const messageDiv = document.createElement('div');
messageDiv.style.cssText = `
color: #ffd700;
font-size: 1.3em;
margin-top: 20px;
font-style: italic;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
`;
genieContainer.appendChild(genieImg);
genieContainer.appendChild(messageDiv);
container.appendChild(genieContainer);
// Animate appearance
setTimeout(() => {
genieContainer.style.opacity = '1';
genieContainer.style.transform = 'scale(1)';
}, 100);
// Add floating sparkles
setTimeout(() => {
this.createFloatingSparkles(genieImg);
}, 800);
// Show message with typewriter effect
setTimeout(() => {
this.typewriterEffect(messageDiv, [message], 80);
}, 1200);
this.currentGenieImage = genieImg;
return { genieContainer, genieImg, messageDiv };
}
// Create brief, vague genie responses with contextual hints
getGenieResponse(questionType = 'general', userProgress = null) {
const baseResponses = {
general: ['Perhaps.', 'Indeed.', 'Seek deeper.', 'Hmm.', 'Time reveals all.'],
wisdom: ['Know yourself.', 'Look within.', 'Truth lies hidden.', 'Journey awaits.'],
future: ['Unclear.', 'Many paths.', 'Soon.', 'Patience.', 'Stars know.'],
help: ['You have power.', 'Trust instincts.', 'Answer within.', 'Keep searching.'],
stuck: ['Path unclear?', 'Seek patience.', 'Try again.', 'Look closer.', 'Different angle.']
};
// Contextual hints based on progress
const contextualHints = {
'aziza_riddle': ['Hidden names.', 'Who am I?', 'Look deeper.'],
'easter_eggs': ['Click around.', 'Explore more.', 'Hidden secrets.'],
'discoveries': ['More to find.', 'Keep exploring.', 'Secrets await.'],
'voting_progress': ['Choose wisely.', 'Voice matters.', 'Direction unclear.'],
'transcendental': ['Beyond sight.', 'Higher plane.', 'Consciousness shifts.']
};
// Check if we can provide a contextual hint
if (userProgress && this.shouldGiveHint(userProgress)) {
for (const [key, hints] of Object.entries(contextualHints)) {
if (this.isUserStuckOn(key, userProgress)) {
return hints[Math.floor(Math.random() * hints.length)];
}
}
}
const responseSet = baseResponses[questionType] || baseResponses.general;
return responseSet[Math.floor(Math.random() * responseSet.length)];
}
// Determine if user needs a hint based on their progress
shouldGiveHint(userProgress) {
// Only give hints if user seems genuinely stuck
return Math.random() < 0.6; // 60% chance of contextual hint
}
// Check if user appears stuck on specific elements
isUserStuckOn(element, userProgress) {
const now = Date.now();
const timeSinceActivity = now - this.lastActivityTime;
switch (element) {
case 'aziza_riddle':
return !localStorage.getItem('aziza_riddle_solved_undefined') && timeSinceActivity > 120000;
case 'easter_eggs':
return userProgress && userProgress.discoveries && userProgress.discoveries.length < 3;
case 'voting_progress':
return document.querySelector('.voting-system') && timeSinceActivity > 180000;
default:
return false;
}
}
// Integrate with multiple free AI APIs with fallback chain
async getAIGenieResponse(question, userProgress = null) {
const systemPrompt = `You are the mystical genie from Aladdin, like Robin Williams' character - wise, ancient, and magical, but you speak VERY briefly. Your responses must be 1-3 words maximum. Be cryptic, mysterious, and helpful but never give direct answers. You have cosmic wisdom but express it in the briefest way possible. Examples: "Perhaps.", "Seek deeper.", "Hidden truths.", "Time reveals.", "Look within.", "Indeed.", "Hmm.", "Patience.". NEVER break character. NEVER give long responses. NEVER be direct.`;
// Try multiple APIs in order
const apis = [
() => this.tryGroqAPI(question, systemPrompt),
() => this.tryHuggingFaceAPI(question, systemPrompt),
() => this.tryOllamaAPI(question, systemPrompt),
() => this.tryCohere(question, systemPrompt)
];
for (const apiCall of apis) {
try {
const response = await apiCall();
// Validate character consistency
if (response && this.validateGenieResponse(response)) {
return response;
}
} catch (error) {
console.log('🧞 API failed, trying next...');
continue;
}
}
// Final fallback to local responses
console.log('🧞 All APIs unavailable, using mystical fallback');
return this.getGenieResponse(this.categorizeQuestion(question), userProgress);
}
// Validate that AI response stays in character
validateGenieResponse(response) {
const cleaned = response.toLowerCase().trim();
// Length check - must be very brief
if (cleaned.length > 25) {
console.log('🧞 Response too long, using fallback');
return false;
}
// Character-breaking phrases to avoid
const forbiddenPhrases = [
'i am an ai', 'as an ai', 'i cannot', 'i am not',
'according to', 'i think', 'in my opinion',
'i would suggest', 'you should', 'i recommend',
'let me explain', 'to be honest', 'actually'
];
for (const phrase of forbiddenPhrases) {
if (cleaned.includes(phrase)) {
console.log('🧞 Response breaks character, using fallback');
return false;
}
}
// Acceptable mystical words/phrases
const mysticalWords = [
'perhaps', 'indeed', 'hmm', 'seek', 'hidden', 'truth',
'time', 'reveals', 'within', 'deeper', 'patience',
'journey', 'path', 'unclear', 'stars', 'know',
'power', 'trust', 'instinct', 'closer', 'angle'
];
// Check if response contains mystical language or is very brief
const isMystical = mysticalWords.some(word => cleaned.includes(word));
const isVeryBrief = cleaned.length <= 10;
return isMystical || isVeryBrief;
}
// Groq API (fastest, free tier)
async tryGroqAPI(question, systemPrompt) {
const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + (localStorage.getItem('groq_api_key') || ''),
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'llama3-8b-8192',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
max_tokens: 10,
temperature: 0.9
})
});
if (response.ok) {
const data = await response.json();
return data.choices[0]?.message?.content?.trim();
}
throw new Error('Groq failed');
}
// Hugging Face Inference API (free)
async tryHuggingFaceAPI(question, systemPrompt) {
const response = await fetch('https://api-inference.huggingface.co/models/microsoft/DialoGPT-medium', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + (localStorage.getItem('hf_api_key') || ''),
'Content-Type': 'application/json'
},
body: JSON.stringify({
inputs: `${systemPrompt}\nUser: ${question}\nGenie:`,
parameters: {
max_length: 15,
temperature: 0.9,
return_full_text: false
}
})
});
if (response.ok) {
const data = await response.json();
return data[0]?.generated_text?.trim();
}
throw new Error('HuggingFace failed');
}
// Local Ollama instance (if user has it running)
async tryOllamaAPI(question, systemPrompt) {
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama2',
prompt: `${systemPrompt}\n\nQuestion: ${question}\nResponse:`,
stream: false,
options: {
num_predict: 5,
temperature: 0.9
}
})
});
if (response.ok) {
const data = await response.json();
return data.response?.trim();
}
throw new Error('Ollama failed');
}
// Cohere API (has free tier)
async tryCohere(question, systemPrompt) {
const response = await fetch('https://api.cohere.ai/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + (localStorage.getItem('cohere_api_key') || ''),
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'command-light',
prompt: `${systemPrompt}\n\nQuestion: ${question}\nResponse:`,
max_tokens: 10,
temperature: 0.9
})
});
if (response.ok) {
const data = await response.json();
return data.generations[0]?.text?.trim();
}
throw new Error('Cohere failed');
}
// Get API key (implement secure storage in production)
getGroqApiKey() {
// This should be stored securely - for demo purposes only
return localStorage.getItem('groq_api_key') || 'your-groq-api-key-here';
}
// Show genie giving wisdom (normal state) with REAL AI integration
async showGenieWisdom(container, question = '') {
// Check if user can ask questions
const canAsk = await this.checkGenieStatus();
if (!canAsk.canAsk) {
this.showDailyLimitMessage();
return;
}
// Show thinking state first
const thinkingGenie = this.showGenieWithMessage(container, '...', true);
try {
// Get REAL AI response from backend
const response = await this.askRealGenieAI(question);
// Update with actual response
setTimeout(() => {
thinkingGenie.messageDiv.innerHTML = '';
this.typewriterEffect(thinkingGenie.messageDiv, [response.response], 80);
// Switch to laughing state when talking
setTimeout(() => {
this.transitionGenieState(thinkingGenie.genieImg, 'laughing', 200);
}, 500);
// Show insights if available
if (response.insights) {
this.showPersonalizedInsights(container, response.insights);
}
}, 1000);
} catch (error) {
// Fallback to local response
const fallbackResponse = this.getGenieResponse('general');
thinkingGenie.messageDiv.innerHTML = '';
this.typewriterEffect(thinkingGenie.messageDiv, [fallbackResponse], 80);
}
return thinkingGenie;
}
// Connect to REAL AI backend API
async askRealGenieAI(question) {
const userId = this.getUserId();
const response = await fetch('/api/genie/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: userId,
question: question,
sessionId: this.getSessionId()
})
});
if (!response.ok) {
throw new Error('Genie API failed');
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Genie unavailable');
}
return data;
}
// Check genie status and remaining questions
async checkGenieStatus() {
try {
const userId = this.getUserId();
const response = await fetch(`/api/genie/status/${userId}`);
const data = await response.json();
if (data.success) {
return {
canAsk: data.canAsk,
remaining: data.remainingQuestions,
limit: data.dailyLimit
};
}
} catch (error) {
console.log('Status check failed, allowing local mode');
}
// Fallback to local limits
return { canAsk: this.canSummonGenie(), remaining: this.dailySummonLimit - this.todaySummons };
}
// Show personalized insights from AI
showPersonalizedInsights(container, insights) {
if (!insights || insights.length === 0) return;
const insightsDiv = document.createElement('div');
insightsDiv.className = 'genie-insights';
insightsDiv.style.cssText = `
margin-top: 20px;
padding: 15px;
background: rgba(102, 122, 234, 0.1);
border-radius: 10px;
border-left: 3px solid #667aea;
font-size: 0.9em;
color: #a0aec0;
font-style: italic;
`;
const insightText = this.formatInsights(insights);
insightsDiv.innerHTML = `✨ The genie senses: ${insightText}`;
container.appendChild(insightsDiv);
// Fade in effect
setTimeout(() => {
insightsDiv.style.opacity = '0';
insightsDiv.style.transform = 'translateY(10px)';
insightsDiv.style.transition = 'all 0.5s ease';
setTimeout(() => {
insightsDiv.style.opacity = '1';
insightsDiv.style.transform = 'translateY(0)';
}, 100);
}, 2000);
}
// Format insights for display
formatInsights(insights) {
const messages = {
'seeker_new': 'A new seeker begins their journey',
'seeker_advanced': 'An experienced explorer of mysteries',
'aziza_stuck': 'Confusion around ancient riddles',
'civic_engaged': 'A voice that participates in the collective',
'discovery_master': 'One who uncovers hidden secrets'
};
return insights.map(insight => messages[insight] || 'Unknown paths').join(', ');
}
// Get or create user ID
getUserId() {
let userId = localStorage.getItem('genie_user_id');
if (!userId) {
userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('genie_user_id', userId);
}
return userId;
}
// Get session ID
getSessionId() {
let sessionId = sessionStorage.getItem('genie_session_id');
if (!sessionId) {
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
sessionStorage.setItem('genie_session_id', sessionId);
}
return sessionId;
}
// Show genie talking/responding (laughing state)
showGenieTalking(container, message) {
return this.showGenieWithMessage(container, message, false);
}
// Categorize question for appropriate response
categorizeQuestion(question) {
const q = question.toLowerCase();
if (q.includes('future') || q.includes('will') || q.includes('when')) return 'future';
if (q.includes('wise') || q.includes('know') || q.includes('truth')) return 'wisdom';
if (q.includes('help') || q.includes('how') || q.includes('should')) return 'help';
return 'general';
}
createSmokeParticle(container) {
const smoke = document.createElement('div');
smoke.textContent = '💨';
smoke.style.cssText = `
position: absolute;
font-size: ${0.8 + Math.random() * 0.4}em;
opacity: 0.7;
pointer-events: none;
left: ${-10 + Math.random() * 20}px;
`;
container.appendChild(smoke);
// Animate smoke particle
let opacity = 0.7;
let yPos = 0;
let xDrift = (Math.random() - 0.5) * 20;
const animateSmoke = () => {
yPos -= 2;
xDrift += (Math.random() - 0.5) * 2;
opacity -= 0.02;
smoke.style.transform = `translateY(${yPos}px) translateX(${xDrift}px)`;
smoke.style.opacity = opacity;
if (opacity > 0) {
requestAnimationFrame(animateSmoke);
} else {
smoke.remove();
}
};
requestAnimationFrame(animateSmoke);
}
// Dramatic genie emergence with particles
createGenieEmergence(container) {
return new Promise((resolve) => {
// Create particle burst
this.createParticleBurst(container, {
count: 50,
colors: ['#667aea', '#ffd700', '#9f7aea'],
duration: 3000
});
// Animated text appearance
this.typewriterEffect(container, [
"The ancient spirit stirs...",
"Mystical energy fills the air...",
"The genie awakens!"
], 1000).then(resolve);
});
}
// Typewriter effect for dramatic text
typewriterEffect(container, messages, speed = 100) {
return new Promise((resolve) => {
const textElement = document.createElement('div');
textElement.style.cssText = `
color: #ffd700;
font-size: 1.2em;
text-align: center;
margin: 20px 0;
min-height: 1.5em;
font-family: monospace;
`;
container.appendChild(textElement);
let messageIndex = 0;
let charIndex = 0;
const typeNextChar = () => {
if (messageIndex < messages.length) {
const currentMessage = messages[messageIndex];
if (charIndex < currentMessage.length) {
textElement.textContent += currentMessage[charIndex];
charIndex++;
setTimeout(typeNextChar, speed);
} else {
// Message complete, pause then move to next
setTimeout(() => {
messageIndex++;
charIndex = 0;
textElement.textContent = '';
if (messageIndex < messages.length) {
typeNextChar();
} else {
// All messages complete
setTimeout(() => {
textElement.remove();
resolve();
}, 1000);
}
}, 1500);
}
}
};
typeNextChar();
});
}
// Particle system for magical effects
setupParticleSystem() {
this.particleCanvas = document.createElement('canvas');
this.particleCanvas.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9998;
opacity: 0;
transition: opacity 0.5s ease;
`;
this.particleCtx = this.particleCanvas.getContext('2d');
// Only add to body when needed
this.particleSystemActive = false;
}
activateParticleSystem() {
if (!this.particleSystemActive) {
document.body.appendChild(this.particleCanvas);
this.resizeParticleCanvas();
this.particleCanvas.style.opacity = '1';
this.particleSystemActive = true;
this.animateParticles();
}
}
deactivateParticleSystem() {
if (this.particleSystemActive) {
this.particleCanvas.style.opacity = '0';
setTimeout(() => {
if (this.particleCanvas.parentNode) {
this.particleCanvas.parentNode.removeChild(this.particleCanvas);
}
this.particleSystemActive = false;
this.particles = [];
}, 500);
}
}
resizeParticleCanvas() {
this.particleCanvas.width = window.innerWidth;
this.particleCanvas.height = window.innerHeight;
}
createParticleBurst(targetElement, options = {}) {
const defaults = {
count: 30,
colors: ['#ffd700', '#667aea'],
duration: 2000,
speed: 5
};
const config = { ...defaults, ...options };
if (!this.particleSystemActive) {
this.activateParticleSystem();
}
const rect = targetElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
for (let i = 0; i < config.count; i++) {
const angle = (Math.PI * 2 * i) / config.count;
const speed = config.speed * (0.5 + Math.random() * 0.5);
this.particles.push({
x: centerX,
y: centerY,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: config.colors[Math.floor(Math.random() * config.colors.length)],
size: 2 + Math.random() * 4,
life: config.duration,
maxLife: config.duration,
decay: 0.98
});
}
}
animateParticles() {
if (!this.particleSystemActive) return;
this.particleCtx.clearRect(0, 0, this.particleCanvas.width, this.particleCanvas.height);
this.particles = this.particles.filter(particle => {
// Update particle
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= particle.decay;
particle.vy *= particle.decay;
particle.life -= 16; // Assuming 60fps
// Draw particle
const alpha = particle.life / particle.maxLife;
this.particleCtx.save();
this.particleCtx.globalAlpha = alpha;
this.particleCtx.fillStyle = particle.color;
this.particleCtx.beginPath();
this.particleCtx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
this.particleCtx.fill();
this.particleCtx.restore();
return particle.life > 0;
});
// Continue animation if particles exist
if (this.particles.length > 0) {
requestAnimationFrame(() => this.animateParticles());
} else if (this.particleSystemActive) {
// Auto-deactivate when no particles remain
setTimeout(() => {
if (this.particles.length === 0) {
this.deactivateParticleSystem();
}
}, 1000);
}
}
// Floating sparkles around genie
createFloatingSparkles(container) {
const sparkleCount = 8;
const sparkles = [];
for (let i = 0; i < sparkleCount; i++) {
const sparkle = document.createElement('div');
sparkle.textContent = ['✧', '✦', '⋆', '✨'][Math.floor(Math.random() * 4)];
sparkle.style.cssText = `
position: absolute;
color: #ffd700;
font-size: ${0.8 + Math.random() * 0.6}em;
pointer-events: none;
opacity: 0.7;
`;
container.appendChild(sparkle);
sparkles.push({
element: sparkle,
angle: (Math.PI * 2 * i) / sparkleCount,
radius: 80 + Math.random() * 40,
speed: 0.02 + Math.random() * 0.01,
bobOffset: Math.random() * Math.PI * 2
});
}
let animationId;
const animateSparkles = (time) => {
sparkles.forEach((sparkle, index) => {
sparkle.angle += sparkle.speed;
const x = Math.cos(sparkle.angle) * sparkle.radius;
const y = Math.sin(sparkle.angle) * sparkle.radius + Math.sin(time * 0.003 + sparkle.bobOffset) * 10;
sparkle.element.style.transform = `translate(${x}px, ${y}px)`;
sparkle.element.style.opacity = 0.5 + Math.sin(time * 0.005 + index) * 0.3;
});
animationId = requestAnimationFrame(animateSparkles);
};
animationId = requestAnimationFrame(animateSparkles);
// Return cleanup function
return () => {
cancelAnimationFrame(animationId);
sparkles.forEach(sparkle => sparkle.element.remove());
};
}
// Genie dialogue text animation
animateGenieText(textElement, text) {
textElement.style.opacity = '0';
textElement.style.transform = 'translateY(20px)';
// Create text reveal effect
const words = text.split(' ');
textElement.innerHTML = words.map(word => `${word}`).join(' ');
const wordElements = textElement.querySelectorAll('span');
// Fade in container
setTimeout(() => {
textElement.style.opacity = '1';
textElement.style.transform = 'translateY(0)';
}, 100);
// Animate words individually
wordElements.forEach((word, index) => {
setTimeout(() => {
word.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
word.style.opacity = '1';
word.style.transform = 'translateY(0)';
}, 200 + index * 100);
});
}
// Cave entrance transition
createCaveTransition() {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, transparent 0%, black 70%);
z-index: 9999;
opacity: 0;
transition: opacity 1s ease;
pointer-events: none;
`;
document.body.appendChild(overlay);
// Animate transition
setTimeout(() => {
overlay.style.opacity = '1';
}, 100);
return {
complete: () => {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 1000);
}
};
}
// Setup dynamic effects for existing elements
setupDynamicEffects() {
// Enhance lamp links when they appear
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.classList && node.classList.contains('mystical-lamp-link')) {
this.createProximityGlow(node.querySelector('.lamp-icon'));
}
if (node.classList && node.classList.contains('lamp-interactive')) {
const smokeEffect = this.createRubbingSmoke(node);
// Hook into rubbing events
node.addEventListener('mousedown', smokeEffect.start);
node.addEventListener('touchstart', smokeEffect.start);
document.addEventListener('mouseup', smokeEffect.stop);
document.addEventListener('touchend', smokeEffect.stop);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
// Initialize animations when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.genieAnimations = new GenieAnimations();
});
// Enhance the existing magic user system with animations
if (window.magicUser) {
// Hook into existing genie summoning
const originalSummonGenie = window.magicUser.summonGenieSpirit;
if (originalSummonGenie) {
window.magicUser.summonGenieSpirit = function(cave) {
// Add particle effects before summoning
if (window.genieAnimations) {
window.genieAnimations.createParticleBurst(cave, {
count: 100,
colors: ['#667aea', '#ffd700', '#9f7aea', '#ff6b6b'],
duration: 4000
});
}
// Call original function
return originalSummonGenie.call(this, cave);
};
}
// Hook into genie interface display
const originalShowGenieInterface = window.magicUser.showGenieInterface;
if (originalShowGenieInterface) {
window.magicUser.showGenieInterface = function(cave) {
// Call original function
const result = originalShowGenieInterface.call(this, cave);
// Add floating sparkles around genie
setTimeout(() => {
const genieAvatar = cave.querySelector('.genie-avatar');
if (genieAvatar && window.genieAnimations) {
window.genieAnimations.createFloatingSparkles(genieAvatar);
}
}, 1000);
return result;
};
}
// Hook into text animation
const originalAnimateGenieResponse = window.magicUser.animateGenieResponse;
if (originalAnimateGenieResponse) {
window.magicUser.animateGenieResponse = function(messageElement, response) {
// Use enhanced text animation
if (window.genieAnimations) {
window.genieAnimations.animateGenieText(messageElement, response);
} else {
// Fallback to original
return originalAnimateGenieResponse.call(this, messageElement, response);
}
};
}
}