Add Void Shop and Void Essence currency system
- Add Void Essence currency earned from petting (+2) and feeding (+3) - Add Void Shop modal with purchasable items: - Voidling eggs (Wisp, Blob, Spark, Ember, Glitch, Crystal) - Void Treats to make all voidlings happy - Star Fragments for bonus essence - Add 2 new voidling types: Glitch (rare green) and Crystal (rare blue) - Add 'Void Tycoon' achievement for 500 Void Essence - Update biodiversity achievement to require all 6 voidling types - Add Void Essence display in header with shop button
This commit is contained in:
@ -43,6 +43,11 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a
|
|||||||
- Added manual time control toggle
|
- Added manual time control toggle
|
||||||
- Added voidling speed multipliers based on time of day (slower at night)
|
- Added voidling speed multipliers based on time of day (slower at night)
|
||||||
- Voidlings naturally become sleepy at night
|
- Voidlings naturally become sleepy at night
|
||||||
|
- Added Void Essence currency - earned by petting (+2) and feeding (+3)
|
||||||
|
- Added Void Shop with purchasable voidling eggs, treats, and items
|
||||||
|
- Added 2 new voidling types: Glitch (rare green) and Crystal (rare blue)
|
||||||
|
- Added "Void Tycoon" achievement for accumulating 500 Void Essence
|
||||||
|
- Updated biodiversity achievement to require all 6 voidling types
|
||||||
|
|
||||||
### Next Steps
|
### Next Steps
|
||||||
1. Add sound effects
|
1. Add sound effects
|
||||||
@ -52,4 +57,4 @@ A whimsical pocket dimension where digital companions called "Voidlings" float a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: Session 4 complete - Added petting interaction and achievements system*
|
*Last updated: Session 5 complete - Added voidling shop with Void Essence currency*
|
||||||
|
|||||||
@ -6,7 +6,7 @@ interface Voidling {
|
|||||||
name: string
|
name: string
|
||||||
color: string
|
color: string
|
||||||
mood: 'idle' | 'happy' | 'curious' | 'sleepy'
|
mood: 'idle' | 'happy' | 'curious' | 'sleepy'
|
||||||
type: 'wisp' | 'blob' | 'spark' | 'ember'
|
type: 'wisp' | 'blob' | 'spark' | 'ember' | 'glitch' | 'crystal'
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
vx: number
|
vx: number
|
||||||
@ -105,6 +105,19 @@ interface UserStats {
|
|||||||
totalFeedings: number
|
totalFeedings: number
|
||||||
totalVoidlings: number
|
totalVoidlings: number
|
||||||
sessions: number
|
sessions: number
|
||||||
|
voidEssence: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShopItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: Voidling['type']
|
||||||
|
color: string
|
||||||
|
glow: string
|
||||||
|
shape: string
|
||||||
|
cost: number
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const VOIDLING_TYPES = [
|
const VOIDLING_TYPES = [
|
||||||
@ -112,6 +125,19 @@ const VOIDLING_TYPES = [
|
|||||||
{ type: 'blob', color: 'bg-cyan-400', glow: 'shadow-cyan-500/50', shape: 'rounded-[40%]', description: 'Bouncy and playful' },
|
{ 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: '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' },
|
{ type: 'ember', color: 'bg-rose-400', glow: 'shadow-rose-500/50', shape: 'rounded-full', description: 'Warm and sleepy' },
|
||||||
|
{ type: 'glitch', color: 'bg-green-400', glow: 'shadow-green-500/50', shape: 'rounded-lg', description: 'Unstable and mysterious' },
|
||||||
|
{ type: 'crystal', color: 'bg-blue-400', glow: 'shadow-blue-500/50', shape: 'rounded-sm', description: 'Sharp and reflective' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const SHOP_ITEMS: ShopItem[] = [
|
||||||
|
{ id: 'wisp_egg', name: 'Wisp Egg', type: 'wisp', color: 'bg-purple-400', glow: 'shadow-purple-500/50', shape: 'rounded-full', cost: 50, description: 'Hatches into a wisp', icon: '🥚' },
|
||||||
|
{ id: 'blob_egg', name: 'Blob Egg', type: 'blob', color: 'bg-cyan-400', glow: 'shadow-cyan-500/50', shape: 'rounded-[40%]', cost: 50, description: 'Hatches into a blob', icon: '🥚' },
|
||||||
|
{ id: 'spark_egg', name: 'Spark Egg', type: 'spark', color: 'bg-amber-400', glow: 'shadow-amber-500/50', shape: 'rounded-sm', cost: 75, description: 'Hatches into a spark', icon: '⚡' },
|
||||||
|
{ id: 'ember_egg', name: 'Ember Egg', type: 'ember', color: 'bg-rose-400', glow: 'shadow-rose-500/50', shape: 'rounded-full', cost: 75, description: 'Hatches into an ember', icon: '🔥' },
|
||||||
|
{ id: 'glitch_egg', name: 'Glitch Egg', type: 'glitch', color: 'bg-green-400', glow: 'shadow-green-500/50', shape: 'rounded-lg', cost: 150, description: 'Rare unstable voidling', icon: '👾' },
|
||||||
|
{ id: 'crystal_egg', name: 'Crystal Egg', type: 'crystal', color: 'bg-blue-400', glow: 'shadow-blue-500/50', shape: 'rounded-sm', cost: 150, description: 'Rare sharp voidling', icon: '💎' },
|
||||||
|
{ id: 'treat', name: 'Void Treat', type: 'wisp', color: 'bg-yellow-400', glow: 'shadow-yellow-500/50', shape: 'rounded-full', cost: 20, description: 'All voidlings become happy', icon: '🍪' },
|
||||||
|
{ id: 'star_fragment', name: 'Star Fragment', type: 'wisp', color: 'bg-white', glow: 'shadow-white/50', shape: 'rounded-full', cost: 200, description: 'Rare: +100 Void Essence!', icon: '⭐' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const ACHIEVEMENTS: Achievement[] = [
|
const ACHIEVEMENTS: Achievement[] = [
|
||||||
@ -120,11 +146,12 @@ const ACHIEVEMENTS: Achievement[] = [
|
|||||||
{ 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 6 types of voidlings', icon: '🌈', condition: (v) => { const types = new Set(v.map(x => x.type)); return types.size >= 6; } },
|
||||||
{ 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 },
|
||||||
|
{ id: 'rich', name: 'Void Tycoon', description: 'Accumulate 500 Void Essence', icon: '💰', condition: (_v, s) => s.voidEssence >= 500 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const MOOD_EMOJIS = {
|
const MOOD_EMOJIS = {
|
||||||
@ -182,10 +209,12 @@ export default function Chamber() {
|
|||||||
totalFeedings: 0,
|
totalFeedings: 0,
|
||||||
totalVoidlings: 0,
|
totalVoidlings: 0,
|
||||||
sessions: 1,
|
sessions: 1,
|
||||||
|
voidEssence: 0,
|
||||||
})
|
})
|
||||||
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('night')
|
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('night')
|
||||||
const [isAutoCycle, setIsAutoCycle] = useState(true)
|
const [isAutoCycle, setIsAutoCycle] = useState(true)
|
||||||
const [cycleProgress, setCycleProgress] = useState(0)
|
const [cycleProgress, setCycleProgress] = useState(0)
|
||||||
|
const [showShop, setShowShop] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedVoidlings = localStorage.getItem('voidlings')
|
const savedVoidlings = localStorage.getItem('voidlings')
|
||||||
@ -411,7 +440,7 @@ export default function Chamber() {
|
|||||||
return v
|
return v
|
||||||
}))
|
}))
|
||||||
setUserStats(prev => {
|
setUserStats(prev => {
|
||||||
const newStats = { ...prev, totalFeedings: prev.totalFeedings + 1 }
|
const newStats = { ...prev, totalFeedings: prev.totalFeedings + 1, voidEssence: prev.voidEssence + 3 }
|
||||||
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
||||||
return newStats
|
return newStats
|
||||||
})
|
})
|
||||||
@ -427,7 +456,7 @@ export default function Chamber() {
|
|||||||
return v
|
return v
|
||||||
}))
|
}))
|
||||||
setUserStats(prev => {
|
setUserStats(prev => {
|
||||||
const newStats = { ...prev, totalPets: prev.totalPets + 1 }
|
const newStats = { ...prev, totalPets: prev.totalPets + 1, voidEssence: prev.voidEssence + 2 }
|
||||||
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
||||||
return newStats
|
return newStats
|
||||||
})
|
})
|
||||||
@ -438,6 +467,43 @@ export default function Chamber() {
|
|||||||
setVoidlings(voidlings.filter(v => v.id !== id))
|
setVoidlings(voidlings.filter(v => v.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function purchaseItem(item: ShopItem) {
|
||||||
|
if (userStats.voidEssence < item.cost) return
|
||||||
|
|
||||||
|
setUserStats(prev => {
|
||||||
|
const newStats = { ...prev, voidEssence: prev.voidEssence - item.cost }
|
||||||
|
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
||||||
|
return newStats
|
||||||
|
})
|
||||||
|
|
||||||
|
if (item.id === 'treat') {
|
||||||
|
setVoidlings(prev => prev.map(v => ({ ...v, mood: 'happy' as const })))
|
||||||
|
} else if (item.id === 'star_fragment') {
|
||||||
|
setUserStats(prev => {
|
||||||
|
const newStats = { ...prev, voidEssence: prev.voidEssence + 100 }
|
||||||
|
localStorage.setItem('voidling_stats', JSON.stringify(newStats))
|
||||||
|
return newStats
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const pos = getRandomPosition()
|
||||||
|
const newVoidling: Voidling = {
|
||||||
|
id: generateId(),
|
||||||
|
name: '???',
|
||||||
|
color: item.color,
|
||||||
|
mood: getRandomMood(),
|
||||||
|
type: item.type,
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
vx: pos.vx,
|
||||||
|
vy: pos.vy,
|
||||||
|
pets: 0,
|
||||||
|
}
|
||||||
|
setVoidlings([...voidlings, newVoidling])
|
||||||
|
setNamingVoidling(newVoidling.id)
|
||||||
|
setNewName('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getMoodAnimation = (mood: Voidling['mood']) => {
|
const getMoodAnimation = (mood: Voidling['mood']) => {
|
||||||
switch (mood) {
|
switch (mood) {
|
||||||
case 'happy': return 'animate-bounce'
|
case 'happy': return 'animate-bounce'
|
||||||
@ -522,6 +588,15 @@ export default function Chamber() {
|
|||||||
🏆 <span className="hidden sm:inline">Achievements</span>
|
🏆 <span className="hidden sm:inline">Achievements</span>
|
||||||
<span className="bg-purple-600 text-xs px-1.5 rounded-full">{unlockedAchievements.length}/{ACHIEVEMENTS.length}</span>
|
<span className="bg-purple-600 text-xs px-1.5 rounded-full">{unlockedAchievements.length}/{ACHIEVEMENTS.length}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowShop(true)}
|
||||||
|
className="px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 rounded-xl text-sm transition-all duration-200 hover:scale-105 border border-zinc-700 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
✨ <span className="hidden sm:inline">Shop</span>
|
||||||
|
<span className="bg-zinc-800 text-yellow-400 text-xs px-1.5 rounded-full flex items-center gap-1">
|
||||||
|
◇{userStats.voidEssence}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={spawnVoidling}
|
onClick={spawnVoidling}
|
||||||
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"
|
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"
|
||||||
@ -683,6 +758,48 @@ export default function Chamber() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showShop && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" onClick={() => setShowShop(false)}>
|
||||||
|
<div className="bg-zinc-900 rounded-2xl p-6 max-w-lg w-full border border-zinc-700" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 via-orange-400 to-pink-400">
|
||||||
|
Void Shop
|
||||||
|
</h2>
|
||||||
|
<button onClick={() => setShowShop(false)} className="text-zinc-500 hover:text-zinc-300">×</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mb-4 p-3 bg-zinc-800/50 rounded-xl">
|
||||||
|
<span className="text-2xl">◇</span>
|
||||||
|
<span className="text-yellow-400 font-bold text-lg">Void Essence: {userStats.voidEssence}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-zinc-500 mb-4">Pet and feed your voidlings to earn Void Essence!</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3 max-h-80 overflow-y-auto">
|
||||||
|
{SHOP_ITEMS.map(item => {
|
||||||
|
const canAfford = userStats.voidEssence >= item.cost
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => purchaseItem(item)}
|
||||||
|
disabled={!canAfford}
|
||||||
|
className={`p-4 rounded-xl border text-left transition-all ${canAfford ? 'bg-zinc-800 border-purple-500/30 hover:border-purple-500 hover:scale-105' : 'bg-zinc-800/30 border-zinc-700 opacity-50 cursor-not-allowed'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className="text-3xl">{item.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-sm">{item.name}</div>
|
||||||
|
<div className="text-xs text-zinc-500">{item.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm font-bold ${canAfford ? 'text-yellow-400' : 'text-zinc-600'}`}>
|
||||||
|
◇{item.cost}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{newAchievement && (
|
{newAchievement && (
|
||||||
<div className="fixed top-8 right-8 bg-gradient-to-r from-purple-600 to-pink-600 p-4 rounded-xl shadow-lg animate-bounce z-50">
|
<div className="fixed top-8 right-8 bg-gradient-to-r from-purple-600 to-pink-600 p-4 rounded-xl shadow-lg animate-bounce z-50">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user