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 voidling speed multipliers based on time of day (slower 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
|
||||
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
|
||||
color: string
|
||||
mood: 'idle' | 'happy' | 'curious' | 'sleepy'
|
||||
type: 'wisp' | 'blob' | 'spark' | 'ember'
|
||||
type: 'wisp' | 'blob' | 'spark' | 'ember' | 'glitch' | 'crystal'
|
||||
x: number
|
||||
y: number
|
||||
vx: number
|
||||
@ -105,6 +105,19 @@ interface UserStats {
|
||||
totalFeedings: number
|
||||
totalVoidlings: 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 = [
|
||||
@ -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: '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: '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[] = [
|
||||
@ -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: '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: '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: '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 },
|
||||
{ id: 'rich', name: 'Void Tycoon', description: 'Accumulate 500 Void Essence', icon: '💰', condition: (_v, s) => s.voidEssence >= 500 },
|
||||
]
|
||||
|
||||
const MOOD_EMOJIS = {
|
||||
@ -182,10 +209,12 @@ export default function Chamber() {
|
||||
totalFeedings: 0,
|
||||
totalVoidlings: 0,
|
||||
sessions: 1,
|
||||
voidEssence: 0,
|
||||
})
|
||||
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('night')
|
||||
const [isAutoCycle, setIsAutoCycle] = useState(true)
|
||||
const [cycleProgress, setCycleProgress] = useState(0)
|
||||
const [showShop, setShowShop] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const savedVoidlings = localStorage.getItem('voidlings')
|
||||
@ -411,7 +440,7 @@ export default function Chamber() {
|
||||
return v
|
||||
}))
|
||||
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))
|
||||
return newStats
|
||||
})
|
||||
@ -427,7 +456,7 @@ export default function Chamber() {
|
||||
return v
|
||||
}))
|
||||
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))
|
||||
return newStats
|
||||
})
|
||||
@ -438,6 +467,43 @@ export default function Chamber() {
|
||||
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']) => {
|
||||
switch (mood) {
|
||||
case 'happy': return 'animate-bounce'
|
||||
@ -522,6 +588,15 @@ export default function Chamber() {
|
||||
🏆 <span className="hidden sm:inline">Achievements</span>
|
||||
<span className="bg-purple-600 text-xs px-1.5 rounded-full">{unlockedAchievements.length}/{ACHIEVEMENTS.length}</span>
|
||||
</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
|
||||
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"
|
||||
@ -683,6 +758,48 @@ export default function Chamber() {
|
||||
</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 && (
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user