diff --git a/app/components/AchievementsModal.tsx b/app/components/AchievementsModal.tsx new file mode 100644 index 0000000..d8fafbd --- /dev/null +++ b/app/components/AchievementsModal.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { useMobileDetection } from '../hooks/useMobileDetection'; +import { abbreviateNumber } from '../utils/numberFormatter'; + +export interface Achievement { + id: string; + name: string; + description: string; + icon: string; + unlocked: boolean; + progress: number; + maxProgress: number; + reward: string; + category: 'clicks' | 'cash' | 'upgrades' | 'special'; +} + +interface AchievementsModalProps { + isOpen: boolean; + onClose: () => void; + achievements: Achievement[]; +} + +const AchievementsModal: React.FC = ({ + isOpen, + onClose, + achievements, +}) => { + const isMobile = useMobileDetection(); + + if (!isOpen) return null; + + const unlockedCount = achievements.filter(a => a.unlocked).length; + const totalCount = achievements.length; + const completionPercentage = ((unlockedCount / totalCount) * 100).toFixed(1); + + const categoryColors = { + clicks: 'from-red-500 to-orange-500', + cash: 'from-green-500 to-emerald-500', + upgrades: 'from-blue-500 to-cyan-500', + special: 'from-purple-500 to-pink-500', + }; + + return ( +
+
+ + {/* Header */} +
+
+
+ 🏆 +
+
+

+ Achievements +

+

+ {unlockedCount}/{totalCount} Unlocked ({completionPercentage}%) +

+
+
+ +
+ + {/* Progress Bar */} +
+
+
+
+
+ + {/* Content Container */} +
+
+ {achievements.map(achievement => { + const progressPercent = (achievement.progress / achievement.maxProgress) * 100; + return ( +
+ {achievement.unlocked && ( +
+ +
+ )} +
+
+ {achievement.icon} +
+
+

+ {achievement.name} +

+

+ {achievement.description} +

+
+
+ + {!achievement.unlocked && ( + <> +
+
+
+

+ Progress: {abbreviateNumber(achievement.progress)}/{abbreviateNumber(achievement.maxProgress)} +

+ + )} + +
+ 🎁 Reward: {achievement.reward} +
+
+ ); + })} +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; + +export default AchievementsModal; diff --git a/app/components/CookieClickerGame.tsx b/app/components/CookieClickerGame.tsx index fe1f932..7e8cee4 100644 --- a/app/components/CookieClickerGame.tsx +++ b/app/components/CookieClickerGame.tsx @@ -38,6 +38,14 @@ interface CookieClickerGameProps { setPopup: Dispatch void; onCancel: () => void } | null>>; initialUpgrades: Upgrade[]; onOpenCasino: () => void; + skillModifiers: { + clickPowerBonus: number; + autoClickerBonus: number; + conversionRateBonus: number; + luckyCrateDiscount: number; + skillPointGain: number; + }; + onStatUpdate: (statName: keyof import('../components/StatisticsModal').Statistics, value: number) => void; } export default function CookieClickerGame({ @@ -45,7 +53,7 @@ export default function CookieClickerGame({ autoClickers, setAutoClickers, luckyCrateCost, setLuckyCrateCost, rebirths, setRebirths, prestigeCurrency, setPrestigeCurrency, upgrades, setUpgrades, notification, setNotification, popup, setPopup, - initialUpgrades, onOpenCasino + initialUpgrades, onOpenCasino, skillModifiers, onStatUpdate }: CookieClickerGameProps) { const isMobile = useMobileDetection(); const [showShopModal, setShowShopModal] = useState(false); @@ -53,15 +61,23 @@ export default function CookieClickerGame({ const handleCookieClick = () => { // Apply prestige multiplier: 1% bonus per prestige currency const prestigeMultiplier = 1 + (prestigeCurrency * 0.01); - const effectiveClickPower = Math.floor(clickPower * prestigeMultiplier); + // Apply skill modifiers + const skillMultiplier = 1 + skillModifiers.clickPowerBonus; + const effectiveClickPower = Math.floor(clickPower * prestigeMultiplier * skillMultiplier); setClicks(prevClicks => prevClicks + effectiveClickPower); + // Track statistics + onStatUpdate('totalClicks', effectiveClickPower); // Lucky crates are now purchased, not random on click }; const convertClicksToCash = () => { - const conversionRate = 0.1 + (rebirths * 0.01); // Base 10% + 1% per rebirth - setCash(prevCash => prevCash + (clicks * conversionRate)); + const baseConversionRate = 0.1 + (rebirths * 0.01); // Base 10% + 1% per rebirth + const conversionRate = baseConversionRate + skillModifiers.conversionRateBonus; + const cashGained = clicks * conversionRate; + setCash(prevCash => prevCash + cashGained); setClicks(0); + // Track statistics + onStatUpdate('totalCashEarned', cashGained); }; const purchaseUpgrade = (upgradeId: string) => { @@ -72,6 +88,8 @@ export default function CookieClickerGame({ setCash(prevCash => Math.max(0, prevCash - upgrade.cost)); upgrade.effect(); setNotification({ message: `Purchased ${upgrade.name}!`, type: 'success' }); + // Track statistics + onStatUpdate('totalUpgradesPurchased', 1); return { ...upgrade, count: upgrade.count + 1, cost: Math.round(upgrade.baseCost * Math.pow(1.15, upgrade.count + 1)) }; } else { setNotification({ message: 'Not enough cash!', type: 'error' }); @@ -83,21 +101,27 @@ export default function CookieClickerGame({ }); }; - // Auto clicker effect with prestige multiplier + // Auto clicker effect with prestige multiplier and skill bonuses useEffect(() => { const interval = setInterval(() => { // Apply prestige multiplier: 1% bonus per prestige currency const prestigeMultiplier = 1 + (prestigeCurrency * 0.01); - const effectiveAutoClickers = Math.floor(autoClickers * prestigeMultiplier); + // Apply skill modifiers + const skillMultiplier = 1 + skillModifiers.autoClickerBonus; + const effectiveAutoClickers = Math.floor(autoClickers * prestigeMultiplier * skillMultiplier); setClicks(prevClicks => prevClicks + effectiveAutoClickers); }, 1000); return () => clearInterval(interval); - }, [autoClickers, prestigeCurrency, setClicks]); + }, [autoClickers, prestigeCurrency, skillModifiers.autoClickerBonus, setClicks]); const purchaseLuckyCrate = () => { - if (cash >= luckyCrateCost) { - setCash(prevCash => Math.max(0, prevCash - luckyCrateCost)); + // Apply skill discount + const discountedCost = Math.floor(luckyCrateCost * (1 - skillModifiers.luckyCrateDiscount)); + if (cash >= discountedCost) { + setCash(prevCash => Math.max(0, prevCash - discountedCost)); setLuckyCrateCost(prevCost => Math.round(prevCost * 1.2)); // Dynamic pricing + // Track statistics + onStatUpdate('totalLuckyCratesOpened', 1); triggerLuckyCrate(); } else { setNotification({ message: 'Not enough cash to buy a Lucky Crate!', type: 'error' }); @@ -122,8 +146,11 @@ export default function CookieClickerGame({ randomOutcome.effect(); }; - // Calculate prestige multiplier + // Calculate prestige and skill multipliers const prestigeMultiplier = 1 + (prestigeCurrency * 0.01); + const clickSkillMultiplier = 1 + skillModifiers.clickPowerBonus; + const autoClickerSkillMultiplier = 1 + skillModifiers.autoClickerBonus; + const effectiveLuckyCrateCost = Math.floor(luckyCrateCost * (1 - skillModifiers.luckyCrateDiscount)); return (
e.preventDefault()}> @@ -193,19 +220,19 @@ export default function CookieClickerGame({

Click Power

-

{abbreviateNumber(Math.floor(clickPower * prestigeMultiplier))}

+

{abbreviateNumber(Math.floor(clickPower * prestigeMultiplier * clickSkillMultiplier))}

Auto Clickers

-

{abbreviateNumber(Math.floor(autoClickers * prestigeMultiplier))}/s

+

{abbreviateNumber(Math.floor(autoClickers * prestigeMultiplier * autoClickerSkillMultiplier))}/s

Conversion Rate

-

{((0.1 + (rebirths * 0.01)) * 100).toFixed(0)}%

+

{(((0.1 + (rebirths * 0.01)) + skillModifiers.conversionRateBonus) * 100).toFixed(0)}%

Crate Cost

-

${abbreviateNumber(luckyCrateCost)}

+

${abbreviateNumber(effectiveLuckyCrateCost)}

@@ -221,7 +248,7 @@ export default function CookieClickerGame({ cash={cash} upgrades={upgrades} onPurchaseUpgrade={purchaseUpgrade} - luckyCrateCost={luckyCrateCost} + luckyCrateCost={effectiveLuckyCrateCost} onPurchaseLuckyCrate={purchaseLuckyCrate} />
diff --git a/app/components/SkillTreeModal.tsx b/app/components/SkillTreeModal.tsx new file mode 100644 index 0000000..78f35f6 --- /dev/null +++ b/app/components/SkillTreeModal.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { useMobileDetection } from '../hooks/useMobileDetection'; +import { abbreviateNumber } from '../utils/numberFormatter'; + +export interface Skill { + id: string; + name: string; + description: string; + cost: number; + maxLevel: number; + currentLevel: number; + prerequisite?: string; // ID of skill that must be unlocked first + category: 'click' | 'idle' | 'economy'; + effect: () => void; +} + +interface SkillTreeModalProps { + isOpen: boolean; + onClose: () => void; + skillPoints: number; + skills: Skill[]; + onUnlockSkill: (skillId: string) => void; +} + +const SkillTreeModal: React.FC = ({ + isOpen, + onClose, + skillPoints, + skills, + onUnlockSkill, +}) => { + const isMobile = useMobileDetection(); + + if (!isOpen) return null; + + const categoryColors = { + click: 'from-red-500 to-orange-500', + idle: 'from-blue-500 to-cyan-500', + economy: 'from-green-500 to-emerald-500', + }; + + const categoryIcons = { + click: '👆', + idle: '⚙️', + economy: '💰', + }; + + const categories: Array<'click' | 'idle' | 'economy'> = ['click', 'idle', 'economy']; + + const isSkillUnlockable = (skill: Skill) => { + if (skill.currentLevel >= skill.maxLevel) return false; + if (skillPoints < skill.cost) return false; + if (skill.prerequisite) { + const prereq = skills.find(s => s.id === skill.prerequisite); + if (!prereq || prereq.currentLevel === 0) return false; + } + return true; + }; + + return ( +
+
+ + {/* Header */} +
+
+
+ 🌳 +
+
+

+ Skill Tree +

+

Available Skill Points: {skillPoints}

+
+
+ +
+ + {/* Content Container */} +
+
+ {categories.map(category => { + const categorySkills = skills.filter(s => s.category === category); + return ( +
+
+
+ {categoryIcons[category]} +
+

+ {category} +

+
+
+ {categorySkills.map(skill => { + const unlockable = isSkillUnlockable(skill); + const maxed = skill.currentLevel >= skill.maxLevel; + return ( +
+
+ + {skill.name} + + + {skill.currentLevel}/{skill.maxLevel} + +
+

+ {skill.description} +

+
+ + 💎 {skill.cost} SP + + +
+
+ ); + })} +
+
+ ); + })} +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; + +export default SkillTreeModal; diff --git a/app/components/StatisticsModal.tsx b/app/components/StatisticsModal.tsx new file mode 100644 index 0000000..855837d --- /dev/null +++ b/app/components/StatisticsModal.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { useMobileDetection } from '../hooks/useMobileDetection'; +import { abbreviateNumber } from '../utils/numberFormatter'; + +export interface Statistics { + totalClicks: number; + totalCashEarned: number; + totalUpgradesPurchased: number; + totalLuckyCratesOpened: number; + totalRebirths: number; + totalPlayTime: number; // in seconds + highestCash: number; + highestClickPower: number; + highestAutoClickers: number; +} + +interface StatisticsModalProps { + isOpen: boolean; + onClose: () => void; + statistics: Statistics; +} + +const StatisticsModal: React.FC = ({ + isOpen, + onClose, + statistics, +}) => { + const isMobile = useMobileDetection(); + + if (!isOpen) return null; + + const formatTime = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + return `${hours}h ${minutes}m ${secs}s`; + }; + + const stats = [ + { icon: '👆', label: 'Total Clicks', value: abbreviateNumber(statistics.totalClicks), color: 'from-red-500 to-orange-500' }, + { icon: '💰', label: 'Total Cash Earned', value: `$${abbreviateNumber(statistics.totalCashEarned)}`, color: 'from-green-500 to-emerald-500' }, + { icon: '📦', label: 'Upgrades Purchased', value: abbreviateNumber(statistics.totalUpgradesPurchased), color: 'from-blue-500 to-cyan-500' }, + { icon: '🎁', label: 'Lucky Crates Opened', value: abbreviateNumber(statistics.totalLuckyCratesOpened), color: 'from-yellow-500 to-orange-500' }, + { icon: '🔄', label: 'Total Rebirths', value: abbreviateNumber(statistics.totalRebirths), color: 'from-purple-500 to-pink-500' }, + { icon: '⏱️', label: 'Total Play Time', value: formatTime(statistics.totalPlayTime), color: 'from-indigo-500 to-blue-500' }, + { icon: '💵', label: 'Highest Cash', value: `$${abbreviateNumber(statistics.highestCash)}`, color: 'from-emerald-500 to-green-600' }, + { icon: '⚡', label: 'Highest Click Power', value: abbreviateNumber(statistics.highestClickPower), color: 'from-yellow-500 to-amber-500' }, + { icon: '🤖', label: 'Highest Auto Clickers', value: abbreviateNumber(statistics.highestAutoClickers), color: 'from-cyan-500 to-blue-500' }, + ]; + + return ( +
+
+ + {/* Header */} +
+
+
+ 📊 +
+

+ Statistics +

+
+ +
+ + {/* Content Container */} +
+
+ {stats.map((stat, index) => { + const borderColor = stat.color.includes('red') ? 'border-red-500/30 hover:border-red-500/60' : + stat.color.includes('green') ? 'border-green-500/30 hover:border-green-500/60' : + stat.color.includes('blue') ? 'border-blue-500/30 hover:border-blue-500/60' : + stat.color.includes('yellow') ? 'border-yellow-500/30 hover:border-yellow-500/60' : + stat.color.includes('purple') ? 'border-purple-500/30 hover:border-purple-500/60' : + stat.color.includes('indigo') ? 'border-indigo-500/30 hover:border-indigo-500/60' : + stat.color.includes('emerald') ? 'border-emerald-500/30 hover:border-emerald-500/60' : + stat.color.includes('amber') ? 'border-amber-500/30 hover:border-amber-500/60' : + 'border-cyan-500/30 hover:border-cyan-500/60'; + + return ( +
+
+
+ {stat.icon} +
+

+ {stat.label} +

+
+

+ {stat.value} +

+
+ ); + })} +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; + +export default StatisticsModal; diff --git a/app/components/TopBar.tsx b/app/components/TopBar.tsx index 4578040..620cd57 100644 --- a/app/components/TopBar.tsx +++ b/app/components/TopBar.tsx @@ -7,14 +7,18 @@ interface TopBarProps { onLoadGame: () => void; onRebirth: () => void; onOpenSettings: () => void; + onOpenSkillTree: () => void; + onOpenStatistics: () => void; + onOpenAchievements: () => void; clicks: number; cash: number; rebirths: number; prestigeCurrency: number; + skillPoints: number; canRebirth: boolean; } -export default function TopBar({ onSaveGame, onLoadGame, onRebirth, onOpenSettings, clicks, cash, rebirths, prestigeCurrency, canRebirth }: TopBarProps) { +export default function TopBar({ onSaveGame, onLoadGame, onRebirth, onOpenSettings, onOpenSkillTree, onOpenStatistics, onOpenAchievements, clicks, cash, rebirths, prestigeCurrency, skillPoints, canRebirth }: TopBarProps) { const isMobile = useMobileDetection(); if (isMobile) { @@ -22,13 +26,16 @@ export default function TopBar({ onSaveGame, onLoadGame, onRebirth, onOpenSettin
{/* Top row with buttons */}
-
- - +
+ + + + +
{canRebirth && ( - + )}
@@ -44,8 +51,8 @@ export default function TopBar({ onSaveGame, onLoadGame, onRebirth, onOpenSettin

${abbreviateNumber(cash)}

-

Rebirths

-

{abbreviateNumber(rebirths)}

+

Skill Points

+

{abbreviateNumber(skillPoints)}

Prestige

@@ -61,6 +68,9 @@ export default function TopBar({ onSaveGame, onLoadGame, onRebirth, onOpenSettin
+ + +
@@ -72,8 +82,8 @@ export default function TopBar({ onSaveGame, onLoadGame, onRebirth, onOpenSettin

${abbreviateNumber(cash)}

-

Rebirths

-

{abbreviateNumber(rebirths)}

+

Skill Points

+

{abbreviateNumber(skillPoints)}

Prestige

diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 333d1c1..d07051c 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -6,6 +6,9 @@ import PopupModal from '../components/PopupModal'; import SettingsModal from '../components/SettingsModal'; import UpdateModal from '../components/UpdateModal'; import BlackjackModal from '../components/BlackjackModal'; +import SkillTreeModal, { type Skill } from '../components/SkillTreeModal'; +import StatisticsModal, { type Statistics } from '../components/StatisticsModal'; +import AchievementsModal, { type Achievement } from '../components/AchievementsModal'; import { checkForNewRelease, setLastSeenVersion } from '../utils/githubRelease'; export default function Home() { @@ -16,23 +19,96 @@ export default function Home() { const [luckyCrateCost, setLuckyCrateCost] = useState(100); // Initial cost for a lucky crate const [rebirths, setRebirths] = useState(0); const [prestigeCurrency, setPrestigeCurrency] = useState(0); + const [skillPoints, setSkillPoints] = useState(0); const [showSettingsModal, setShowSettingsModal] = useState(false); const [showBlackjackModal, setShowBlackjackModal] = useState(false); + const [showSkillTreeModal, setShowSkillTreeModal] = useState(false); + const [showStatisticsModal, setShowStatisticsModal] = useState(false); + const [showAchievementsModal, setShowAchievementsModal] = useState(false); // Settings state const [soundEnabled, setSoundEnabled] = useState(true); const [theme, setTheme] = useState<'dark' | 'light' | 'blue'>('dark'); const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); + // Statistics state + const [statistics, setStatistics] = useState({ + totalClicks: 0, + totalCashEarned: 0, + totalUpgradesPurchased: 0, + totalLuckyCratesOpened: 0, + totalRebirths: 0, + totalPlayTime: 0, + highestCash: 0, + highestClickPower: 1, + highestAutoClickers: 0, + }); + + // Skill modifiers + const [skillModifiers, setSkillModifiers] = useState({ + clickPowerBonus: 0, + autoClickerBonus: 0, + conversionRateBonus: 0, + luckyCrateDiscount: 0, + skillPointGain: 0, + }); + + // Skill tree initialization + const createInitialSkills = (): Skill[] => [ + // Click Path + { id: 'click_power_1', name: 'Power Click I', description: '+5% click power per level', cost: 1, maxLevel: 5, currentLevel: 0, category: 'click', effect: () => setSkillModifiers(prev => ({ ...prev, clickPowerBonus: prev.clickPowerBonus + 0.05 })) }, + { id: 'click_power_2', name: 'Power Click II', description: '+10% click power per level', cost: 2, maxLevel: 5, currentLevel: 0, prerequisite: 'click_power_1', category: 'click', effect: () => setSkillModifiers(prev => ({ ...prev, clickPowerBonus: prev.clickPowerBonus + 0.10 })) }, + { id: 'click_critical', name: 'Critical Clicks', description: '+20% click power per level', cost: 3, maxLevel: 3, currentLevel: 0, prerequisite: 'click_power_2', category: 'click', effect: () => setSkillModifiers(prev => ({ ...prev, clickPowerBonus: prev.clickPowerBonus + 0.20 })) }, + + // Idle Path + { id: 'idle_boost_1', name: 'Idle Boost I', description: '+5% auto clicker efficiency per level', cost: 1, maxLevel: 5, currentLevel: 0, category: 'idle', effect: () => setSkillModifiers(prev => ({ ...prev, autoClickerBonus: prev.autoClickerBonus + 0.05 })) }, + { id: 'idle_boost_2', name: 'Idle Boost II', description: '+10% auto clicker efficiency per level', cost: 2, maxLevel: 5, currentLevel: 0, prerequisite: 'idle_boost_1', category: 'idle', effect: () => setSkillModifiers(prev => ({ ...prev, autoClickerBonus: prev.autoClickerBonus + 0.10 })) }, + { id: 'idle_master', name: 'Idle Master', description: '+25% auto clicker efficiency per level', cost: 3, maxLevel: 3, currentLevel: 0, prerequisite: 'idle_boost_2', category: 'idle', effect: () => setSkillModifiers(prev => ({ ...prev, autoClickerBonus: prev.autoClickerBonus + 0.25 })) }, + + // Economy Path + { id: 'economy_1', name: 'Better Deals I', description: '+2% conversion rate per level', cost: 1, maxLevel: 5, currentLevel: 0, category: 'economy', effect: () => setSkillModifiers(prev => ({ ...prev, conversionRateBonus: prev.conversionRateBonus + 0.02 })) }, + { id: 'economy_2', name: 'Better Deals II', description: '+5% conversion rate per level', cost: 2, maxLevel: 5, currentLevel: 0, prerequisite: 'economy_1', category: 'economy', effect: () => setSkillModifiers(prev => ({ ...prev, conversionRateBonus: prev.conversionRateBonus + 0.05 })) }, + { id: 'lucky_discount', name: 'Lucky Bargain', description: '-10% lucky crate cost per level', cost: 2, maxLevel: 5, currentLevel: 0, prerequisite: 'economy_1', category: 'economy', effect: () => setSkillModifiers(prev => ({ ...prev, luckyCrateDiscount: prev.luckyCrateDiscount + 0.10 })) }, + ]; + + const [skills, setSkills] = useState(createInitialSkills()); + + // Achievements initialization + const createInitialAchievements = (): Achievement[] => [ + { id: 'first_click', name: 'First Click', description: 'Click 1 time', icon: '👆', unlocked: false, progress: 0, maxProgress: 1, reward: '+1 Skill Point', category: 'clicks' }, + { id: 'hundred_clicks', name: 'Century Clicker', description: 'Click 100 times', icon: '👏', unlocked: false, progress: 0, maxProgress: 100, reward: '+2 Skill Points', category: 'clicks' }, + { id: 'thousand_clicks', name: 'Click Master', description: 'Click 1,000 times', icon: '⚡', unlocked: false, progress: 0, maxProgress: 1000, reward: '+5 Skill Points', category: 'clicks' }, + { id: 'million_clicks', name: 'Click God', description: 'Click 1,000,000 times', icon: '🔥', unlocked: false, progress: 0, maxProgress: 1000000, reward: '+10 Skill Points', category: 'clicks' }, + + { id: 'first_cash', name: 'Getting Started', description: 'Earn $100 cash', icon: '💵', unlocked: false, progress: 0, maxProgress: 100, reward: '+1 Skill Point', category: 'cash' }, + { id: 'wealthy', name: 'Wealthy', description: 'Earn $10,000 cash', icon: '💰', unlocked: false, progress: 0, maxProgress: 10000, reward: '+3 Skill Points', category: 'cash' }, + { id: 'millionaire', name: 'Millionaire', description: 'Earn $1,000,000 cash', icon: '💎', unlocked: false, progress: 0, maxProgress: 1000000, reward: '+10 Skill Points', category: 'cash' }, + + { id: 'first_upgrade', name: 'First Purchase', description: 'Buy 1 upgrade', icon: '🛒', unlocked: false, progress: 0, maxProgress: 1, reward: '+1 Skill Point', category: 'upgrades' }, + { id: 'upgrade_spree', name: 'Shopping Spree', description: 'Buy 50 upgrades', icon: '🛍️', unlocked: false, progress: 0, maxProgress: 50, reward: '+5 Skill Points', category: 'upgrades' }, + { id: 'upgrade_master', name: 'Master Collector', description: 'Buy 200 upgrades', icon: '📦', unlocked: false, progress: 0, maxProgress: 200, reward: '+10 Skill Points', category: 'upgrades' }, + + { id: 'first_rebirth', name: 'Born Again', description: 'Complete 1 rebirth', icon: '🔄', unlocked: false, progress: 0, maxProgress: 1, reward: '+5 Skill Points', category: 'special' }, + { id: 'lucky_first', name: 'Lucky Try', description: 'Open 1 lucky crate', icon: '🎁', unlocked: false, progress: 0, maxProgress: 1, reward: '+2 Skill Points', category: 'special' }, + { id: 'lucky_addict', name: 'Gambling Problem', description: 'Open 100 lucky crates', icon: '🎰', unlocked: false, progress: 0, maxProgress: 100, reward: '+10 Skill Points', category: 'special' }, + ]; + + const [achievements, setAchievements] = useState(createInitialAchievements()); + const initialUpgrades = [ - { id: 'power', name: 'Increase Click Power', baseCost: 10, cost: 10, effect: () => setClickPower(prev => prev + 1), count: 0, description: 'Increases the number of clicks you get per click.' }, - { id: 'auto', name: 'Buy Auto Clicker', baseCost: 100, cost: 100, effect: () => setAutoClickers(prev => prev + 1), count: 0, description: 'Automatically clicks for you every second.' }, - { id: 'grandma', name: 'Grandma', baseCost: 1000, cost: 1000, effect: () => setAutoClickers(prev => prev + 10), count: 0, description: 'A nice grandma who bakes cookies for you.' }, - { id: 'farm', name: 'Farm', baseCost: 10000, cost: 10000, effect: () => setAutoClickers(prev => prev + 100), count: 0, description: 'A farm to grow more cookies.' }, - { id: 'mine', name: 'Mine', baseCost: 100000, cost: 100000, effect: () => setAutoClickers(prev => prev + 1000), count: 0, description: 'A mine to extract valuable cookie ore.' }, - { id: 'factory', name: 'Factory', baseCost: 1000000, cost: 1000000, effect: () => setAutoClickers(prev => prev + 10000), count: 0, description: 'A factory to mass produce cookies.' }, - { id: 'bank', name: 'Bank', baseCost: 10000000, cost: 10000000, effect: () => setAutoClickers(prev => prev + 100000), count: 0, description: 'A bank to store and generate interest on your cookies.' }, - { id: 'temple', name: 'Temple', baseCost: 100000000, cost: 100000000, effect: () => setAutoClickers(prev => prev + 1000000), count: 0, description: 'A temple to worship the cookie gods.' }, + { id: 'power', name: 'Increase Click Power', baseCost: 15, cost: 15, effect: () => setClickPower(prev => prev + 1), count: 0, description: 'Increases the number of clicks you get per click.' }, + { id: 'power_2', name: 'Enhanced Cursor', baseCost: 75, cost: 75, effect: () => setClickPower(prev => prev + 3), count: 0, description: 'A more efficient clicking tool.' }, + { id: 'auto', name: 'Buy Auto Clicker', baseCost: 150, cost: 150, effect: () => setAutoClickers(prev => prev + 1), count: 0, description: 'Automatically clicks for you every second.' }, + { id: 'auto_2', name: 'Helper Bot', baseCost: 500, cost: 500, effect: () => setAutoClickers(prev => prev + 5), count: 0, description: 'A small bot that helps click faster.' }, + { id: 'grandma', name: 'Grandma', baseCost: 2000, cost: 2000, effect: () => setAutoClickers(prev => prev + 10), count: 0, description: 'A nice grandma who bakes cookies for you.' }, + { id: 'farm', name: 'Farm', baseCost: 15000, cost: 15000, effect: () => setAutoClickers(prev => prev + 50), count: 0, description: 'A farm to grow more cookies.' }, + { id: 'factory', name: 'Small Factory', baseCost: 75000, cost: 75000, effect: () => setAutoClickers(prev => prev + 200), count: 0, description: 'A small factory to produce cookies.' }, + { id: 'mine', name: 'Mine', baseCost: 250000, cost: 250000, effect: () => setAutoClickers(prev => prev + 500), count: 0, description: 'A mine to extract valuable cookie ore.' }, + { id: 'big_factory', name: 'Big Factory', baseCost: 1000000, cost: 1000000, effect: () => setAutoClickers(prev => prev + 2000), count: 0, description: 'A large factory to mass produce cookies.' }, + { id: 'lab', name: 'Laboratory', baseCost: 5000000, cost: 5000000, effect: () => setAutoClickers(prev => prev + 10000), count: 0, description: 'A lab that researches new cookie technology.' }, + { id: 'bank', name: 'Bank', baseCost: 20000000, cost: 20000000, effect: () => setAutoClickers(prev => prev + 50000), count: 0, description: 'A bank to store and generate interest on your cookies.' }, + { id: 'temple', name: 'Temple', baseCost: 100000000, cost: 100000000, effect: () => setAutoClickers(prev => prev + 250000), count: 0, description: 'A temple to worship the cookie gods.' }, + { id: 'portal', name: 'Portal', baseCost: 500000000, cost: 500000000, effect: () => setAutoClickers(prev => prev + 1000000), count: 0, description: 'A portal to another dimension full of cookies.' }, ]; const [upgrades, setUpgrades] = useState(initialUpgrades); const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' | 'info' | 'warning' } | null>(null); @@ -49,7 +125,12 @@ export default function Home() { luckyCrateCost, rebirths, prestigeCurrency, + skillPoints, upgrades, + skills, + achievements, + statistics, + skillModifiers, settings: { soundEnabled, theme, @@ -58,19 +139,20 @@ export default function Home() { }; localStorage.setItem('cookieClickerGame', JSON.stringify(gameState)); setNotification({ message: 'Game Saved!', type: 'success' }); - }, [clicks, cash, clickPower, autoClickers, luckyCrateCost, rebirths, prestigeCurrency, upgrades, soundEnabled, theme, autoSaveEnabled]); + }, [clicks, cash, clickPower, autoClickers, luckyCrateCost, rebirths, prestigeCurrency, skillPoints, upgrades, skills, achievements, statistics, skillModifiers, soundEnabled, theme, autoSaveEnabled]); const loadGame = useCallback(() => { const savedState = localStorage.getItem('cookieClickerGame'); if (savedState) { const gameState = JSON.parse(savedState); - setClicks(gameState.clicks); - setCash(gameState.cash); - setClickPower(gameState.clickPower); - setAutoClickers(gameState.autoClickers); - setLuckyCrateCost(gameState.luckyCrateCost); - setRebirths(gameState.rebirths); - setPrestigeCurrency(gameState.prestigeCurrency); + setClicks(gameState.clicks || 0); + setCash(gameState.cash || 0); + setClickPower(gameState.clickPower || 1); + setAutoClickers(gameState.autoClickers || 0); + setLuckyCrateCost(gameState.luckyCrateCost || 100); + setRebirths(gameState.rebirths || 0); + setPrestigeCurrency(gameState.prestigeCurrency || 0); + setSkillPoints(gameState.skillPoints || 0); // Load settings if they exist if (gameState.settings) { @@ -79,6 +161,30 @@ export default function Home() { setAutoSaveEnabled(gameState.settings.autoSaveEnabled ?? true); } + // Load statistics if they exist + if (gameState.statistics) { + setStatistics(gameState.statistics); + } + + // Load skill modifiers if they exist + if (gameState.skillModifiers) { + setSkillModifiers(gameState.skillModifiers); + } + + // Load skills with effect functions + if (gameState.skills) { + const loadedSkills = gameState.skills.map((loadedSkill: any) => { + const initialSkill = createInitialSkills().find(s => s.id === loadedSkill.id); + return initialSkill ? { ...loadedSkill, effect: initialSkill.effect } : loadedSkill; + }); + setSkills(loadedSkills); + } + + // Load achievements + if (gameState.achievements) { + setAchievements(gameState.achievements); + } + // Re-assign effect functions to loaded upgrades const loadedUpgradesWithEffects = gameState.upgrades.map((loadedUpgrade: any) => { const initialUpgrade = initialUpgrades.find(iu => iu.id === loadedUpgrade.id); @@ -95,6 +201,116 @@ export default function Home() { loadGame(); // Load game on component mount }, [loadGame]); + // Helper function to check and unlock achievements + const checkAchievements = useCallback(() => { + setAchievements(prevAchievements => { + const updated = prevAchievements.map(achievement => { + if (achievement.unlocked) return achievement; + + let currentProgress = achievement.progress; + + // Update progress based on achievement type + switch (achievement.id) { + case 'first_click': + case 'hundred_clicks': + case 'thousand_clicks': + case 'million_clicks': + currentProgress = statistics.totalClicks; + break; + case 'first_cash': + case 'wealthy': + case 'millionaire': + currentProgress = statistics.totalCashEarned; + break; + case 'first_upgrade': + case 'upgrade_spree': + case 'upgrade_master': + currentProgress = statistics.totalUpgradesPurchased; + break; + case 'first_rebirth': + currentProgress = statistics.totalRebirths; + break; + case 'lucky_first': + case 'lucky_addict': + currentProgress = statistics.totalLuckyCratesOpened; + break; + } + + const newAchievement = { ...achievement, progress: currentProgress }; + + // Check if achievement should be unlocked + if (currentProgress >= achievement.maxProgress && !achievement.unlocked) { + newAchievement.unlocked = true; + // Award skill points based on achievement reward + const pointsMatch = achievement.reward.match(/\+(\d+) Skill Point/); + if (pointsMatch) { + const points = parseInt(pointsMatch[1]); + setSkillPoints(prev => prev + points); + setNotification({ + message: `🏆 Achievement Unlocked: ${achievement.name}! (+${points} Skill Points)`, + type: 'success' + }); + } + } + + return newAchievement; + }); + return updated; + }); + }, [statistics]); + + // Check achievements whenever statistics change + useEffect(() => { + checkAchievements(); + }, [checkAchievements]); + + // Update statistics whenever relevant values change + useEffect(() => { + setStatistics(prev => ({ + ...prev, + highestCash: Math.max(prev.highestCash, cash), + highestClickPower: Math.max(prev.highestClickPower, clickPower), + highestAutoClickers: Math.max(prev.highestAutoClickers, autoClickers), + })); + }, [cash, clickPower, autoClickers]); + + // Track play time + useEffect(() => { + const interval = setInterval(() => { + setStatistics(prev => ({ ...prev, totalPlayTime: prev.totalPlayTime + 1 })); + }, 1000); + return () => clearInterval(interval); + }, []); + + // Skill unlock handler + const handleUnlockSkill = useCallback((skillId: string) => { + setSkills(prevSkills => { + const updatedSkills = prevSkills.map(skill => { + if (skill.id === skillId) { + if (skillPoints >= skill.cost && skill.currentLevel < skill.maxLevel) { + const canUnlock = !skill.prerequisite || + (prevSkills.find(s => s.id === skill.prerequisite)?.currentLevel ?? 0) > 0; + + if (canUnlock) { + setSkillPoints(prev => prev - skill.cost); + skill.effect(); + setNotification({ message: `Unlocked ${skill.name}!`, type: 'success' }); + return { ...skill, currentLevel: skill.currentLevel + 1 }; + } else { + setNotification({ message: 'Prerequisite skill required!', type: 'error' }); + } + } else if (skillPoints < skill.cost) { + setNotification({ message: 'Not enough skill points!', type: 'error' }); + } else { + setNotification({ message: 'Skill already maxed!', type: 'info' }); + } + } + return skill; + }); + return updatedSkills; + }); + }, [skillPoints]); + // Auto-save every 30 seconds if enabled useEffect(() => { if (autoSaveEnabled) { @@ -123,14 +339,27 @@ export default function Home() { }, []); const handleRebirth = () => { + // Update rebirth statistics + setStatistics(prev => ({ ...prev, totalRebirths: prev.totalRebirths + 1 })); + setRebirths(prev => prev + 1); - setPrestigeCurrency(prev => prev + Math.floor(cash / 1000000)); // 1 prestige currency per 1,000,000 cash + const prestigeGained = Math.floor(cash / 1000000); // 1 prestige currency per 1,000,000 cash + setPrestigeCurrency(prev => prev + prestigeGained); + + // Grant skill points on rebirth (1 per rebirth) + setSkillPoints(prev => prev + 1); + setClicks(0); setCash(0); setClickPower(1); setAutoClickers(0); + setLuckyCrateCost(100); setUpgrades(initialUpgrades); // Reset upgrades - setNotification({ message: 'Rebirth successful! You gained prestige currency!', type: 'success' }); + + setNotification({ + message: `Rebirth successful! +${prestigeGained} Prestige, +1 Skill Point!`, + type: 'success' + }); setPopup(null); }; @@ -151,7 +380,28 @@ export default function Home() { setLuckyCrateCost(100); setRebirths(0); setPrestigeCurrency(0); + setSkillPoints(0); setUpgrades(initialUpgrades); + setSkills(createInitialSkills()); + setAchievements(createInitialAchievements()); + setStatistics({ + totalClicks: 0, + totalCashEarned: 0, + totalUpgradesPurchased: 0, + totalLuckyCratesOpened: 0, + totalRebirths: 0, + totalPlayTime: 0, + highestCash: 0, + highestClickPower: 1, + highestAutoClickers: 0, + }); + setSkillModifiers({ + clickPowerBonus: 0, + autoClickerBonus: 0, + conversionRateBonus: 0, + luckyCrateDiscount: 0, + skillPointGain: 0, + }); setSoundEnabled(true); setTheme('dark'); setAutoSaveEnabled(true); @@ -191,15 +441,19 @@ export default function Home() { onLoadGame={loadGame} onRebirth={() => setPopup({ title: 'Rebirth Confirmation', - message: 'Are you sure you want to Rebirth? You will lose all clicks, cash, and upgrades, but gain prestige currency!', + message: 'Are you sure you want to Rebirth? You will lose all clicks, cash, and upgrades, but gain prestige currency and skill points!', onConfirm: handleRebirth, onCancel: () => setPopup(null), })} onOpenSettings={handleOpenSettings} + onOpenSkillTree={() => setShowSkillTreeModal(true)} + onOpenStatistics={() => setShowStatisticsModal(true)} + onOpenAchievements={() => setShowAchievementsModal(true)} clicks={clicks} cash={cash} rebirths={rebirths} prestigeCurrency={prestigeCurrency} + skillPoints={skillPoints} canRebirth={canRebirth} /> setShowBlackjackModal(true)} + skillModifiers={skillModifiers} + onStatUpdate={(statName, value) => { + setStatistics(prev => ({ + ...prev, + [statName]: prev[statName] + value + })); + }} /> {notification && ( )} + + setShowSkillTreeModal(false)} + skillPoints={skillPoints} + skills={skills} + onUnlockSkill={handleUnlockSkill} + /> + + setShowStatisticsModal(false)} + statistics={statistics} + /> + + setShowAchievementsModal(false)} + achievements={achievements} + />
); } diff --git a/package-lock.json b/package-lock.json index 2610947..f335058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1140,7 +1139,6 @@ "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.10.1.tgz", "integrity": "sha512-qYco7sFpbRgoKJKsCgJmFBQwaLVsLv255K8vbPodnXe13YBEzV/ugIqRCYVz2hghvlPiEKgaHh2On0s/5npn6w==", "license": "MIT", - "peer": true, "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.1", @@ -1821,7 +1819,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1832,7 +1829,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2050,7 +2046,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2539,7 +2534,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3929,7 +3923,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3939,7 +3932,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3962,7 +3954,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -4488,7 +4479,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4571,7 +4561,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4676,7 +4665,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4820,7 +4808,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" },