import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' interface Voidling { id: string name: string color: string mood: 'idle' | 'happy' | 'curious' | 'sleepy' type: 'wisp' | 'blob' | 'spark' | 'ember' x: number y: number vx: number vy: number } interface Particle { id: number x: number y: number size: number speed: number opacity: number color: string } const VOIDLING_TYPES = [ { type: 'wisp', color: 'bg-purple-400', glow: 'shadow-purple-500/50', shape: 'rounded-full', description: 'Ethereal and floaty' }, { type: 'blob', color: 'bg-cyan-400', glow: 'shadow-cyan-500/50', shape: 'rounded-[40%]', description: 'Bouncy and playful' }, { type: 'spark', color: 'bg-amber-400', glow: 'shadow-amber-500/50', shape: 'rounded-sm', description: 'Energetic and quick' }, { type: 'ember', color: 'bg-rose-400', glow: 'shadow-rose-500/50', shape: 'rounded-full', description: 'Warm and sleepy' }, ] const MOOD_EMOJIS = { idle: '◉', happy: '✧', curious: '❓', sleepy: '💤', } const PARTICLE_COLORS = ['bg-purple-400/30', 'bg-cyan-400/30', 'bg-pink-400/30', 'bg-amber-400/30'] function getRandomType(): Voidling['type'] { const types: Voidling['type'][] = ['wisp', 'blob', 'spark', 'ember'] return types[Math.floor(Math.random() * types.length)] } function getTypeStyle(type: Voidling['type']) { return VOIDLING_TYPES.find(t => t.type === type) || VOIDLING_TYPES[0] } function getRandomPosition() { return { x: Math.random() * 80 + 10, y: Math.random() * 60 + 20, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, } } function getRandomMood(): Voidling['mood'] { const moods: Voidling['mood'][] = ['idle', 'happy', 'curious', 'sleepy'] return moods[Math.floor(Math.random() * moods.length)] } function generateId() { return Math.random().toString(36).substring(2, 9) } function getTypeInfo(type: Voidling['type']) { return VOIDLING_TYPES.find(t => t.type === type) || VOIDLING_TYPES[0] } export default function Chamber() { const [voidlings, setVoidlings] = useState([]) const [namingVoidling, setNamingVoidling] = useState(null) const [newName, setNewName] = useState('') const [particles, setParticles] = useState([]) const [feedingId, setFeedingId] = useState(null) useEffect(() => { const saved = localStorage.getItem('voidlings') if (saved) { try { setVoidlings(JSON.parse(saved)) } catch { createStarterVoidlings() } } else { createStarterVoidlings() } }, []) useEffect(() => { localStorage.setItem('voidlings', JSON.stringify(voidlings)) }, [voidlings]) useEffect(() => { const interval = setInterval(() => { setParticles(prev => { const newParticles: Particle[] = prev .map(p => ({ ...p, y: p.y - p.speed, opacity: p.opacity - 0.005, })) .filter(p => p.opacity > 0) if (newParticles.length < 30 && Math.random() > 0.7) { newParticles.push({ id: Date.now() + Math.random(), x: Math.random() * 100, y: 100, size: Math.random() * 4 + 2, speed: Math.random() * 0.3 + 0.1, opacity: Math.random() * 0.5 + 0.3, color: PARTICLE_COLORS[Math.floor(Math.random() * PARTICLE_COLORS.length)], }) } return newParticles }) }, 50) return () => clearInterval(interval) }, []) useEffect(() => { const interval = setInterval(() => { setVoidlings(prev => prev.map(v => { let { x, y, vx, vy } = v if (v.mood === 'sleepy') { vx *= 0.95 vy *= 0.95 } else if (v.mood === 'happy') { vx += (Math.random() - 0.5) * 0.2 vy += (Math.random() - 0.5) * 0.2 } else if (v.mood === 'curious') { vx += (Math.random() - 0.5) * 0.1 vy += (Math.random() - 0.5) * 0.1 } if (v.type === 'spark') { vx *= 1.02 vy *= 1.02 if (Math.abs(vx) > 1) vx = vx > 0 ? 1 : -1 if (Math.abs(vy) > 1) vy = vy > 0 ? 1 : -1 } x += vx y += vy if (x < 5 || x > 95) { vx *= -1; x = Math.max(5, Math.min(95, x)) } if (y < 10 || y > 90) { vy *= -1; y = Math.max(10, Math.min(90, y)) } return { ...v, x, y, vx, vy } })) }, 100) return () => clearInterval(interval) }, []) function createStarterVoidlings() { const starters: Voidling[] = [ { id: generateId(), name: 'Glip', color: 'bg-purple-400', mood: 'idle', type: 'wisp', ...getRandomPosition() }, { id: generateId(), name: 'Floop', color: 'bg-cyan-400', mood: 'happy', type: 'blob', ...getRandomPosition() }, { id: generateId(), name: 'Norf', color: 'bg-pink-400', mood: 'curious', type: 'spark', ...getRandomPosition() }, ] setVoidlings(starters) } function spawnVoidling() { const pos = getRandomPosition() const type = getRandomType() const typeStyles = getTypeStyle(type) const newVoidling: Voidling = { id: generateId(), name: '???', color: typeStyles.color, mood: getRandomMood(), type: type, x: pos.x, y: pos.y, vx: pos.vx, vy: pos.vy, } setVoidlings([...voidlings, newVoidling]) setNamingVoidling(newVoidling.id) setNewName('') } function startNaming(id: string) { setNamingVoidling(id) const v = voidlings.find(v => v.id === id) setNewName(v?.name === '???' ? '' : v?.name || '') } function finishNaming(id: string) { if (newName.trim()) { setVoidlings(voidlings.map(v => v.id === id ? { ...v, name: newName.trim() } : v )) } setNamingVoidling(null) setNewName('') } function changeMood(id: string) { const moods: Voidling['mood'][] = ['idle', 'happy', 'curious', 'sleepy'] setVoidlings(voidlings.map(v => { if (v.id === id) { const currentIndex = moods.indexOf(v.mood) const nextMood = moods[(currentIndex + 1) % moods.length] return { ...v, mood: nextMood } } return v })) } function feedVoidling(id: string) { setFeedingId(id) setVoidlings(voidlings.map(v => { if (v.id === id) { return { ...v, mood: 'happy', vy: v.vy - 0.5 } } return v })) setTimeout(() => setFeedingId(null), 500) } function releaseVoidling(id: string) { setVoidlings(voidlings.filter(v => v.id !== id)) } const getMoodAnimation = (mood: Voidling['mood']) => { switch (mood) { case 'happy': return 'animate-bounce' case 'sleepy': return 'animate-pulse' case 'curious': return 'animate-ping' default: return '' } } return (
{particles.map(p => (
))}
← Back to Portal

The Void Chamber

{voidlings.length === 0 && (

The chamber feels empty... Summon a voidling to begin.

)} {voidlings.map((voidling) => { const typeInfo = getTypeInfo(voidling.type) return (
changeMood(voidling.id)} onDoubleClick={(e) => { e.stopPropagation(); startNaming(voidling.id) }} >
{MOOD_EMOJIS[voidling.mood]}
{namingVoidling === voidling.id ? ( setNewName(e.target.value)} onBlur={() => finishNaming(voidling.id)} onKeyDown={(e) => { if (e.key === 'Enter') finishNaming(voidling.id) }} className="bg-zinc-800 border border-zinc-600 rounded-lg px-2 py-1 text-xs text-center w-24 focus:outline-none focus:border-purple-400" onClick={(e) => e.stopPropagation()} /> ) : ( <> {voidling.name} {voidling.type} )}
) })}

Click: change mood • Double-click: rename • ◕: feed • ×: release

Your voidlings: {voidlings.length}

{VOIDLING_TYPES.map(t => ( {t.type} ))}
) }