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:
11
WORKLOG.md
11
WORKLOG.md
@ -37,13 +37,18 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a
|
||||
- Added petting interaction (♡ button) - makes voidlings happy
|
||||
- Added achievements system with 10 unlockable achievements
|
||||
- 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
|
||||
1. Add sound effects
|
||||
2. Add voidling shop/trading
|
||||
3. Add night/day cycle
|
||||
4. Add more voidling types
|
||||
5. Add voidling minigames
|
||||
3. Add more voidling types
|
||||
4. Add voidling minigames
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -14,6 +14,74 @@ interface Voidling {
|
||||
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 {
|
||||
id: number
|
||||
x: number
|
||||
@ -47,13 +115,13 @@ const VOIDLING_TYPES = [
|
||||
]
|
||||
|
||||
const ACHIEVEMENTS: Achievement[] = [
|
||||
{ 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: '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: 'collector', name: 'Collector', description: 'Have 5 different voidlings', icon: '📦', condition: (v, s) => v.length >= 5 },
|
||||
{ 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: '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: '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: '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: '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 },
|
||||
@ -115,6 +183,9 @@ export default function Chamber() {
|
||||
totalVoidlings: 0,
|
||||
sessions: 1,
|
||||
})
|
||||
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('night')
|
||||
const [isAutoCycle, setIsAutoCycle] = useState(true)
|
||||
const [cycleProgress, setCycleProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const savedVoidlings = localStorage.getItem('voidlings')
|
||||
@ -170,6 +241,43 @@ export default function Chamber() {
|
||||
})
|
||||
}, [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(() => {
|
||||
const interval = setInterval(() => {
|
||||
setParticles(prev => {
|
||||
@ -199,6 +307,9 @@ export default function Chamber() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const currentTimeCycle = TIME_CYCLES.find(t => t.name === timeOfDay) || TIME_CYCLES[0]
|
||||
const multiplier = currentTimeCycle.voidlingMultiplier
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setVoidlings(prev => prev.map(v => {
|
||||
let { x, y, vx, vy } = v
|
||||
@ -207,11 +318,11 @@ export default function Chamber() {
|
||||
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
|
||||
vx += (Math.random() - 0.5) * 0.2 * multiplier
|
||||
vy += (Math.random() - 0.5) * 0.2 * multiplier
|
||||
} else if (v.mood === 'curious') {
|
||||
vx += (Math.random() - 0.5) * 0.1
|
||||
vy += (Math.random() - 0.5) * 0.1
|
||||
vx += (Math.random() - 0.5) * 0.1 * multiplier
|
||||
vy += (Math.random() - 0.5) * 0.1 * multiplier
|
||||
}
|
||||
|
||||
if (v.type === 'spark') {
|
||||
@ -221,8 +332,8 @@ export default function Chamber() {
|
||||
if (Math.abs(vy) > 1) vy = vy > 0 ? 1 : -1
|
||||
}
|
||||
|
||||
x += vx
|
||||
y += vy
|
||||
x += vx * multiplier
|
||||
y += vy * multiplier
|
||||
|
||||
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)) }
|
||||
@ -231,7 +342,7 @@ export default function Chamber() {
|
||||
}))
|
||||
}, 100)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
}, [timeOfDay])
|
||||
|
||||
function createStarterVoidlings() {
|
||||
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 (
|
||||
<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">
|
||||
{particles.map(p => (
|
||||
<div
|
||||
@ -369,6 +483,38 @@ export default function Chamber() {
|
||||
The Void Chamber
|
||||
</h1>
|
||||
<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
|
||||
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"
|
||||
@ -385,11 +531,11 @@ export default function Chamber() {
|
||||
</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 top-10 left-10 w-32 h-32 bg-purple-500/10 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 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-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 ${currentTimeCycle.orb2} rounded-full blur-2xl animate-pulse delay-500`} />
|
||||
<div className={`absolute top-1/2 left-1/3 w-24 h-24 ${currentTimeCycle.orb3} rounded-full blur-2xl animate-pulse delay-1000`} />
|
||||
</div>
|
||||
|
||||
{voidlings.length === 0 && (
|
||||
|
||||
Reference in New Issue
Block a user