Add night/day cycle with 4 time phases

- Added time cycle system with dawn, day, dusk, and night phases
- Each phase has unique gradient backgrounds and ambient orb colors
- Voidlings move faster during day/dawn/dusk, slower at night
- Voidlings naturally become sleepy when night falls
- Added auto-cycle that progresses every ~15 seconds per phase
- Added manual time control toggle for user preference
- Added progress bar showing time until next phase
- Time controls display current time icon and name
This commit is contained in:
Ernie Cook
2026-03-02 17:44:50 -05:00
parent ffa9efb800
commit 3a7335ea7c
2 changed files with 172 additions and 21 deletions

View File

@ -37,13 +37,18 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a
- Added petting interaction (♡ button) - makes voidlings happy - Added petting interaction (♡ button) - makes voidlings happy
- Added achievements system with 10 unlockable achievements - Added achievements system with 10 unlockable achievements
- Added user stats tracking (total pets, feedings, sessions) - Added user stats tracking (total pets, feedings, sessions)
- Added night/day cycle with 4 time phases (dawn, day, dusk, night)
- Added time-based visual themes with different gradients and ambient colors
- Added automatic time progression (1 minute per full cycle)
- Added manual time control toggle
- Added voidling speed multipliers based on time of day (slower at night)
- Voidlings naturally become sleepy at night
### Next Steps ### Next Steps
1. Add sound effects 1. Add sound effects
2. Add voidling shop/trading 2. Add voidling shop/trading
3. Add night/day cycle 3. Add more voidling types
4. Add more voidling types 4. Add voidling minigames
5. Add voidling minigames
--- ---

View File

@ -14,6 +14,74 @@ interface Voidling {
pets: number pets: number
} }
type TimeOfDay = 'dawn' | 'day' | 'dusk' | 'night'
interface TimeCycle {
name: TimeOfDay
gradient: string
chamberBg: string
chamberBorder: string
orb1: string
orb2: string
orb3: string
voidlingMultiplier: number
description: string
}
const TIME_CYCLES: TimeCycle[] = [
{
name: 'dawn',
gradient: 'from-orange-300 via-pink-300 to-purple-400',
chamberBg: 'bg-orange-900/20',
chamberBorder: 'border-orange-700/30',
orb1: 'bg-orange-400/20',
orb2: 'bg-pink-400/20',
orb3: 'bg-purple-400/20',
voidlingMultiplier: 1.2,
description: 'The void stirs...'
},
{
name: 'day',
gradient: 'from-cyan-300 via-blue-300 to-purple-300',
chamberBg: 'bg-cyan-900/20',
chamberBorder: 'border-cyan-700/30',
orb1: 'bg-cyan-400/20',
orb2: 'bg-blue-400/20',
orb3: 'bg-purple-400/20',
voidlingMultiplier: 1.5,
description: 'The void is bright'
},
{
name: 'dusk',
gradient: 'from-purple-400 via-pink-400 to-orange-400',
chamberBg: 'bg-purple-900/20',
chamberBorder: 'border-purple-700/30',
orb1: 'bg-purple-400/20',
orb2: 'bg-pink-400/20',
orb3: 'bg-orange-400/20',
voidlingMultiplier: 1.2,
description: 'Shadows lengthen...'
},
{
name: 'night',
gradient: 'from-indigo-900 via-purple-900 to-black',
chamberBg: 'bg-indigo-950/40',
chamberBorder: 'border-indigo-800/40',
orb1: 'bg-indigo-500/10',
orb2: 'bg-purple-500/10',
orb3: 'bg-pink-500/10',
voidlingMultiplier: 0.5,
description: 'The void sleeps'
},
]
const TIME_ICONS: Record<TimeOfDay, string> = {
dawn: '🌅',
day: '☀️',
dusk: '🌆',
night: '🌙',
}
interface Particle { interface Particle {
id: number id: number
x: number x: number
@ -47,13 +115,13 @@ const VOIDLING_TYPES = [
] ]
const ACHIEVEMENTS: Achievement[] = [ const ACHIEVEMENTS: Achievement[] = [
{ id: 'first_pet', name: 'Gentle Touch', description: 'Pet your first voidling', icon: '✋', condition: (v, s) => s.totalPets >= 1 }, { id: 'first_pet', name: 'Gentle Touch', description: 'Pet your first voidling', icon: '✋', condition: (_v, s) => s.totalPets >= 1 },
{ id: 'ten_pets', name: 'Frienemy', description: 'Pet voidlings 10 times', icon: '🤝', condition: (v, s) => s.totalPets >= 10 }, { id: 'ten_pets', name: 'Frienemy', description: 'Pet voidlings 10 times', icon: '🤝', condition: (_v, s) => s.totalPets >= 10 },
{ id: 'fifty_pets', name: 'Void Whisperer', description: 'Pet voidlings 50 times', icon: '🔮', condition: (v, s) => s.totalPets >= 50 }, { id: 'fifty_pets', name: 'Void Whisperer', description: 'Pet voidlings 50 times', icon: '🔮', condition: (_v, s) => s.totalPets >= 50 },
{ id: 'first_feeding', name: 'Nourisher', description: 'Feed your first voidling', icon: '◕', condition: (v, s) => s.totalFeedings >= 1 }, { id: 'first_feeding', name: 'Nourisher', description: 'Feed your first voidling', icon: '◕', condition: (_v, s) => s.totalFeedings >= 1 },
{ id: 'collector', name: 'Collector', description: 'Have 5 different voidlings', icon: '📦', condition: (v, s) => v.length >= 5 }, { id: 'collector', name: 'Collector', description: 'Have 5 different voidlings', icon: '📦', condition: (v, _s) => v.length >= 5 },
{ id: 'variety', name: 'Biodiversity', description: 'Have all 4 types of voidlings', icon: '🌈', condition: (v) => { const types = new Set(v.map(x => x.type)); return types.size >= 4; } }, { id: 'variety', name: 'Biodiversity', description: 'Have all 4 types of voidlings', icon: '🌈', condition: (v) => { const types = new Set(v.map(x => x.type)); return types.size >= 4; } },
{ id: 'parent', name: 'Protective Parent', description: 'Have 10 voidlings', icon: '👨‍👩‍👧‍👦', condition: (v, s) => v.length >= 10 }, { id: 'parent', name: 'Protective Parent', description: 'Have 10 voidlings', icon: '👨‍👩‍👧‍👦', condition: (v, _s) => v.length >= 10 },
{ id: 'curious', name: 'Curious Mind', description: 'Have a curious voidling', icon: '❓', condition: (v) => v.some(x => x.mood === 'curious') }, { id: 'curious', name: 'Curious Mind', description: 'Have a curious voidling', icon: '❓', condition: (v) => v.some(x => x.mood === 'curious') },
{ id: 'sleepy_boys', name: 'Sleepy Boys', description: 'Have a sleepy voidling', icon: '😴', condition: (v) => v.some(x => x.mood === 'sleepy') }, { id: 'sleepy_boys', name: 'Sleepy Boys', description: 'Have a sleepy voidling', icon: '😴', condition: (v) => v.some(x => x.mood === 'sleepy') },
{ id: 'happy_home', name: 'Happy Home', description: 'Have 3 happy voidlings', icon: '☺️', condition: (v) => v.filter(x => x.mood === 'happy').length >= 3 }, { id: 'happy_home', name: 'Happy Home', description: 'Have 3 happy voidlings', icon: '☺️', condition: (v) => v.filter(x => x.mood === 'happy').length >= 3 },
@ -115,6 +183,9 @@ export default function Chamber() {
totalVoidlings: 0, totalVoidlings: 0,
sessions: 1, sessions: 1,
}) })
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('night')
const [isAutoCycle, setIsAutoCycle] = useState(true)
const [cycleProgress, setCycleProgress] = useState(0)
useEffect(() => { useEffect(() => {
const savedVoidlings = localStorage.getItem('voidlings') const savedVoidlings = localStorage.getItem('voidlings')
@ -170,6 +241,43 @@ export default function Chamber() {
}) })
}, [voidlings, userStats]) }, [voidlings, userStats])
useEffect(() => {
if (!isAutoCycle) return
const cycleDuration = 60000
const updateInterval = 500
const interval = setInterval(() => {
setCycleProgress(prev => {
const newProgress = prev + (updateInterval / cycleDuration) * TIME_CYCLES.length
if (newProgress >= TIME_CYCLES.length) {
return 0
}
return newProgress
})
}, updateInterval)
return () => clearInterval(interval)
}, [isAutoCycle])
useEffect(() => {
const currentIndex = Math.floor(cycleProgress)
if (TIME_CYCLES[currentIndex]) {
setTimeOfDay(TIME_CYCLES[currentIndex].name)
}
}, [cycleProgress])
useEffect(() => {
if (timeOfDay === 'night') {
setVoidlings(prev => prev.map(v => {
if (v.mood !== 'sleepy' && Math.random() > 0.7) {
return { ...v, mood: 'sleepy' as const }
}
return v
}))
}
}, [timeOfDay])
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setParticles(prev => { setParticles(prev => {
@ -199,6 +307,9 @@ export default function Chamber() {
}, []) }, [])
useEffect(() => { useEffect(() => {
const currentTimeCycle = TIME_CYCLES.find(t => t.name === timeOfDay) || TIME_CYCLES[0]
const multiplier = currentTimeCycle.voidlingMultiplier
const interval = setInterval(() => { const interval = setInterval(() => {
setVoidlings(prev => prev.map(v => { setVoidlings(prev => prev.map(v => {
let { x, y, vx, vy } = v let { x, y, vx, vy } = v
@ -207,11 +318,11 @@ export default function Chamber() {
vx *= 0.95 vx *= 0.95
vy *= 0.95 vy *= 0.95
} else if (v.mood === 'happy') { } else if (v.mood === 'happy') {
vx += (Math.random() - 0.5) * 0.2 vx += (Math.random() - 0.5) * 0.2 * multiplier
vy += (Math.random() - 0.5) * 0.2 vy += (Math.random() - 0.5) * 0.2 * multiplier
} else if (v.mood === 'curious') { } else if (v.mood === 'curious') {
vx += (Math.random() - 0.5) * 0.1 vx += (Math.random() - 0.5) * 0.1 * multiplier
vy += (Math.random() - 0.5) * 0.1 vy += (Math.random() - 0.5) * 0.1 * multiplier
} }
if (v.type === 'spark') { if (v.type === 'spark') {
@ -221,8 +332,8 @@ export default function Chamber() {
if (Math.abs(vy) > 1) vy = vy > 0 ? 1 : -1 if (Math.abs(vy) > 1) vy = vy > 0 ? 1 : -1
} }
x += vx x += vx * multiplier
y += vy y += vy * multiplier
if (x < 5 || x > 95) { vx *= -1; x = Math.max(5, Math.min(95, x)) } 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)) } if (y < 10 || y > 90) { vy *= -1; y = Math.max(10, Math.min(90, y)) }
@ -231,7 +342,7 @@ export default function Chamber() {
})) }))
}, 100) }, 100)
return () => clearInterval(interval) return () => clearInterval(interval)
}, []) }, [timeOfDay])
function createStarterVoidlings() { function createStarterVoidlings() {
const starters: Voidling[] = [ const starters: Voidling[] = [
@ -336,8 +447,11 @@ export default function Chamber() {
} }
} }
const currentTimeCycle = TIME_CYCLES.find(t => t.name === timeOfDay) || TIME_CYCLES[0]
const progressInPhase = cycleProgress % 1
return ( return (
<div className="min-h-screen p-8 relative overflow-hidden"> <div className={`min-h-screen p-8 relative overflow-hidden transition-colors duration-1000 bg-gradient-to-br ${currentTimeCycle.gradient}`}>
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none">
{particles.map(p => ( {particles.map(p => (
<div <div
@ -369,6 +483,38 @@ export default function Chamber() {
The Void Chamber The Void Chamber
</h1> </h1>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex items-center gap-2 px-3 py-2 bg-zinc-800/80 rounded-xl border border-zinc-700">
<span className="text-lg" title={currentTimeCycle.description}>{TIME_ICONS[timeOfDay]}</span>
<div className="flex flex-col">
<span className="text-xs capitalize text-zinc-300">{timeOfDay}</span>
<div className="w-16 h-1 bg-zinc-700 rounded-full overflow-hidden">
<div
className="h-full bg-purple-400 transition-all duration-300"
style={{ width: `${progressInPhase * 100}%` }}
/>
</div>
</div>
<button
onClick={() => setIsAutoCycle(!isAutoCycle)}
className={`text-xs px-2 py-1 rounded ${isAutoCycle ? 'bg-purple-600' : 'bg-zinc-600'} hover:bg-purple-500 transition-colors`}
title={isAutoCycle ? 'Auto cycle on' : 'Auto cycle off'}
>
{isAutoCycle ? 'AUTO' : 'MANUAL'}
</button>
{!isAutoCycle && (
<div className="flex gap-1">
{TIME_CYCLES.map((t) => (
<button
key={t.name}
onClick={() => setTimeOfDay(t.name)}
className={`text-xs px-2 py-1 rounded transition-colors ${timeOfDay === t.name ? 'bg-purple-600' : 'bg-zinc-600 hover:bg-zinc-500'}`}
>
{TIME_ICONS[t.name]}
</button>
))}
</div>
)}
</div>
<button <button
onClick={() => setShowAchievements(!showAchievements)} onClick={() => setShowAchievements(!showAchievements)}
className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-sm transition-all duration-200 hover:scale-105 border border-zinc-700 flex items-center gap-2" className="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-sm transition-all duration-200 hover:scale-105 border border-zinc-700 flex items-center gap-2"
@ -385,11 +531,11 @@ export default function Chamber() {
</div> </div>
</div> </div>
<div className="relative w-full h-[70vh] bg-zinc-900/30 rounded-3xl border border-zinc-800 overflow-hidden"> <div className={`relative w-full h-[70vh] rounded-3xl border overflow-hidden transition-colors duration-1000 ${currentTimeCycle.chamberBg} ${currentTimeCycle.chamberBorder}`}>
<div className="absolute inset-0"> <div className="absolute inset-0">
<div className="absolute top-10 left-10 w-32 h-32 bg-purple-500/10 rounded-full blur-2xl animate-pulse" /> <div className={`absolute top-10 left-10 w-32 h-32 ${currentTimeCycle.orb1} rounded-full blur-2xl animate-pulse`} />
<div className="absolute bottom-20 right-20 w-40 h-40 bg-cyan-500/10 rounded-full blur-2xl animate-pulse delay-500" /> <div className={`absolute bottom-20 right-20 w-40 h-40 ${currentTimeCycle.orb2} rounded-full blur-2xl animate-pulse delay-500`} />
<div className="absolute top-1/2 left-1/3 w-24 h-24 bg-pink-500/10 rounded-full blur-2xl animate-pulse delay-1000" /> <div className={`absolute top-1/2 left-1/3 w-24 h-24 ${currentTimeCycle.orb3} rounded-full blur-2xl animate-pulse delay-1000`} />
</div> </div>
{voidlings.length === 0 && ( {voidlings.length === 0 && (