import React, { useState, useEffect, useCallback } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot, updateDoc, query, addDoc } from 'firebase/firestore'; import { Ghost, Map as MapIcon, ShoppingBag, BookOpen, Settings, Sparkles, Skull, Coins, Moon, ChevronRight, Loader2, Heart, Zap, Coffee, Hand } from 'lucide-react'; // --- CONFIGURATION --- const apiKey = ""; // Provided by environment const appId = typeof __app_id !== 'undefined' ? __app_id : 'spooky-veil-001'; const firebaseConfig = JSON.parse(__firebase_config); // --- FIREBASE INIT --- const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // --- CONSTANTS --- const LOCATIONS = [ { id: 'woods', name: 'Whispering Woods', icon: '🌲', lore: 'Trees that moan and paths that shift. Watch your step, or the roots might find you.', color: 'bg-emerald-900' }, { id: 'library', name: 'Sunken Library', icon: '📚', lore: 'Ancient texts floating in ink. The ghosts here demand silence—or your soul.', color: 'bg-indigo-900' }, { id: 'manor', name: 'Hollow Manor', icon: '🏰', lore: 'A dinner party that never ended. The tea is cold, but the guests are quite lively.', color: 'bg-purple-900' }, { id: 'graveyard', name: 'Restless Graves', icon: '🪦', lore: 'The pets here are just looking for a friend. Or a snack.', color: 'bg-slate-900' }, { id: 'shop', name: 'Spectral Emporium', icon: '🏪', lore: 'Currency: Ectoplasm. Quality: Questionable.', color: 'bg-amber-900' }, ]; const SPECIES = [ { type: 'Ghost Kitty', description: 'Translucent and surprisingly cuddly.' }, { type: 'Shadow Hound', description: 'Loyal, but tends to merge with corners.' }, { type: 'Spirit Owl', description: 'Always watching. Always knowing.' }, { type: 'Wisp Rabbit', description: 'Faster than a blink, brighter than a candle.' } ]; // --- UTILS --- const sleep = (ms) => new Promise(res => setTimeout(res, ms)); const fetchWithRetry = async (url, options, retries = 5) => { for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (response.ok) return await response.json(); if (response.status === 429) { await sleep(Math.pow(2, i) * 1000); continue; } throw new Error(`API Error: ${response.status}`); } catch (err) { if (i === retries - 1) throw err; await sleep(Math.pow(2, i) * 1000); } } }; // --- COMPONENTS --- const LoadingScreen = ({ message }) => (

{message || "Summoning the Veil..."}

); export default function App() { const [user, setUser] = useState(null); const [userData, setUserData] = useState(null); const [pets, setPets] = useState([]); const [activeTab, setActiveTab] = useState('map'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeStory, setActiveStory] = useState(null); const [storyLoading, setStoryLoading] = useState(false); // 1. AUTH INITIALIZATION useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (err) { console.error("Auth Error:", err); setError("Failed to breach the Veil. Try refreshing."); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (u) => { setUser(u); if (!u) setLoading(false); }); return () => unsubscribe(); }, []); // 2. DATA SUBSCRIPTION useEffect(() => { if (!user) return; const userRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const petsRef = collection(db, 'artifacts', appId, 'users', user.uid, 'pets'); const unsubUser = onSnapshot(userRef, (snap) => { if (snap.exists()) { setUserData(snap.data()); } else { // Init profile setDoc(userRef, { username: `Spirit_${user.uid.slice(0, 5)}`, ectoplasm: 500, inventory: [], stats: { explorations: 0, petsFound: 0 } }); } setLoading(false); }, (err) => setError("Lore retrieval failed.")); const unsubPets = onSnapshot(petsRef, (snap) => { const p = snap.docs.map(d => ({ id: d.id, ...d.data() })); setPets(p); }, (err) => setError("Pet manifest lost.")); return () => { unsubUser(); unsubPets(); }; }, [user]); // 3. ACTIONS const generatePetImage = async (species) => { try { const prompt = `A stylish cartoon paranormal pet, ${species}, glowing neon teal and purple outline, spooky cute aesthetic, clean vector lines, high quality illustration, white background.`; const response = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=${apiKey}`, { method: 'POST', body: JSON.stringify({ instances: { prompt }, parameters: { sampleCount: 1 } }) }); return `data:image/png;base64,${response.predictions[0].bytesBase64Encoded}`; } catch (e) { return "https://via.placeholder.com/400x400/1a1a2e/teal?text=Ghost+Pet"; } }; const adoptPet = async () => { if (userData.ectoplasm < 100) return alert("Not enough Ectoplasm!"); setLoading(true); const species = SPECIES[Math.floor(Math.random() * SPECIES.length)]; const img = await generatePetImage(species.type); await addDoc(collection(db, 'artifacts', appId, 'users', user.uid, 'pets'), { name: `${species.type} ${Math.floor(Math.random() * 999)}`, species: species.type, image: img, happiness: 100, hunger: 0, level: 1, born: Date.now() }); const userRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await updateDoc(userRef, { ectoplasm: userData.ectoplasm - 100, 'stats.petsFound': (userData.stats.petsFound || 0) + 1 }); setLoading(false); }; const startStory = async (location) => { setStoryLoading(true); setActiveTab('story'); try { const systemPrompt = `You are a spooky dungeon master. Create a short 'Choose Your Own Adventure' snippet (2 paragraphs) set in ${location.name}. Theme: stylish cartoon paranormal. Provide 3 specific choices for the player. Respond in JSON format: { "text": "...", "choices": ["choice 1", "choice 2", "choice 3"], "reward": 20 }`; const res = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', body: JSON.stringify({ contents: [{ parts: [{ text: "Tell me a new story segment." }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json" } }) }); const story = JSON.parse(res.candidates[0].content.parts[0].text); setActiveStory(story); } catch (e) { setError("The spirits are quiet right now."); } finally { setStoryLoading(false); } }; const makeChoice = async (reward) => { const userRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await updateDoc(userRef, { ectoplasm: userData.ectoplasm + reward, 'stats.explorations': (userData.stats.explorations || 0) + 1 }); setActiveStory(null); setActiveTab('map'); }; // ===== [NEW FEATURE: PET INTERACTIONS] ===== // Added by: Imogen (Gemini) // Purpose: Allow users to pet, feed, and use spirits to harvest ectoplasm. const feedPet = async (petId, petData) => { if (userData.ectoplasm < 25) return alert("Not enough Ectoplasm to feed this spirit!"); setLoading(true); try { const petRef = doc(db, 'artifacts', appId, 'users', user.uid, 'pets', petId); await updateDoc(petRef, { hunger: Math.max(0, (petData.hunger || 0) - 40), happiness: Math.min(100, (petData.happiness || 0) + 15) }); const userRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await updateDoc(userRef, { ectoplasm: userData.ectoplasm - 25 }); } finally { setLoading(false); } }; const petSpirit = async (petId, petData) => { setLoading(true); try { const petRef = doc(db, 'artifacts', appId, 'users', user.uid, 'pets', petId); await updateDoc(petRef, { happiness: Math.min(100, (petData.happiness || 0) + 20) }); } finally { setLoading(false); } }; const hauntWithSpirit = async (petId, petData) => { if ((petData.happiness || 0) < 20) return alert("Your spirit is too miserable to haunt anyone!"); setLoading(true); try { const petRef = doc(db, 'artifacts', appId, 'users', user.uid, 'pets', petId); await updateDoc(petRef, { happiness: Math.max(0, (petData.happiness || 0) - 30), hunger: Math.min(100, (petData.hunger || 0) + 20) }); const userRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await updateDoc(userRef, { ectoplasm: userData.ectoplasm + 50 }); } finally { setLoading(false); } }; // ===== END PET INTERACTIONS ===== if (loading) return ; if (error) return

{error}

; return (
{/* Header */} {/* Main Content */}
{activeTab === 'map' && (

The Ethereal Enclave

Explore the realms between. Every shadow tells a story, and every wisp demands a price.

{LOCATIONS.map(loc => ( ))}
)} {activeTab === 'pets' && (

Your Spirits

{pets.length === 0 ? (

The graveyard is empty...

Adopt your first ghost to begin your journey through the Veil.

) : (
{pets.map(pet => (
{pet.name}

{pet.name}

{pet.species}
Lv. {pet.level}
{/* [UPDATED: Interaction Buttons] */}
))}
)}
)} {activeTab === 'story' && (
{storyLoading ? (

The Oracle is peering into your future...

) : activeStory ? (

{activeStory.text}

{activeStory.choices.map((choice, i) => ( ))}
) : (

No story currently unfolding.

)}
)} {activeTab === 'stats' && (

Manifesto of Souls

{[ { label: 'Explorations', val: userData?.stats?.explorations || 0, icon: }, { label: 'Spirits Found', val: userData?.stats?.petsFound || 0, icon: }, { label: 'Ectoplasm', val: userData?.ectoplasm || 0, icon: }, { label: 'Days Active', val: '1', icon: }, ].map((stat, i) => (
{stat.icon}
{stat.val} {stat.label}
))}

Registry Details

Unique Identifier {user?.uid}
Current Alias {userData?.username}
)}
{/* Navigation Bar (Mobile-first) */}
{/* Floating Sparkles for Vibe */}
); }