diff --git a/WORKLOG.md b/WORKLOG.md index 51ec671..43db075 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -13,6 +13,9 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a - [x] Main Chamber view created with floating voidlings - [x] Ability to name and rename voidlings - [x] localStorage persistence for voidlings +- [x] Particle effects and ambient glows added +- [x] Voidling types/classes with different behaviors +- [x] Mood-specific animations ### In Progress (None yet) @@ -26,14 +29,19 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a - Added ability to summon, name, rename, and release voidlings - Added localStorage persistence to remember voidlings between sessions - Added mood system (idle, happy, curious, sleepy) with click interaction +- Added particle effects and ambient glows for atmosphere +- Added 4 voidling types: Wisp (ethereal), Blob (bouncy), Spark (energetic), Ember (warm) +- Added voidling behaviors: spark moves faster, sleepy slows down, happy gets energetic +- Added mood-specific animations (bounce, pulse, ping) +- Added feeding interaction ### Next Steps -1. Add more visual effects (particles, glows) -2. Add voidling interactions (feeding, playing) -3. Add sound effects -4. Add voidling types/classes with different behaviors -5. Add animations for different moods +1. Add sound effects +2. Add more voidling interactions (playing, petting) +3. Add achievements or collection system +4. Add voidling shop/trading +5. Add night/day cycle --- -*Last updated: Session 2 complete - Chamber view with interactive voidlings added* +*Last updated: Session 3 complete - Added particle effects, voidling types, and mood animations* diff --git a/src/pages/Chamber.tsx b/src/pages/Chamber.tsx index 974db2f..0cd7c53 100644 --- a/src/pages/Chamber.tsx +++ b/src/pages/Chamber.tsx @@ -6,17 +6,28 @@ interface Voidling { name: string color: string mood: 'idle' | 'happy' | 'curious' | 'sleepy' + type: 'wisp' | 'blob' | 'spark' | 'ember' x: number y: number + vx: number + vy: number } -const VOIDLING_COLORS = [ - 'bg-purple-400', - 'bg-cyan-400', - 'bg-pink-400', - 'bg-emerald-400', - 'bg-amber-400', - 'bg-violet-400', +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 = { @@ -26,14 +37,23 @@ const MOOD_EMOJIS = { sleepy: '💤', } -function getRandomColor() { - return VOIDLING_COLORS[Math.floor(Math.random() * VOIDLING_COLORS.length)] +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, } } @@ -46,10 +66,16 @@ 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') @@ -68,24 +94,92 @@ export default function Chamber() { 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', ...getRandomPosition() }, - { id: generateId(), name: 'Floop', color: 'bg-cyan-400', mood: 'happy', ...getRandomPosition() }, - { id: generateId(), name: 'Norf', color: 'bg-pink-400', mood: 'curious', ...getRandomPosition() }, + { 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: getRandomColor(), + 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) @@ -120,16 +214,49 @@ export default function Chamber() { })) } + 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 => ( +
+ ))} +
+
+
@@ -152,79 +279,113 @@ export default function Chamber() {
+
+
+
+
+
+ {voidlings.length === 0 && (

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

)} - {voidlings.map((voidling) => ( -
changeMood(voidling.id)} - onDoubleClick={() => startNaming(voidling.id)} - > + {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} - - )} -
+
+ {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 voidling to change mood • Double-click to rename • Click × to release

+

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

Your voidlings: {voidlings.length}

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