Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions app/components/AchievementsModal.tsx
Original file line number Diff line number Diff line change
@@ -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<AchievementsModalProps> = ({
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 (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4">
<div className={`bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 text-white ${
isMobile
? 'p-5 mx-2 max-w-sm h-5/6'
: 'p-8 max-w-5xl max-h-5/6'
} w-full rounded-3xl shadow-2xl overflow-hidden border-2 border-purple-500/30`}>

{/* Header */}
<div className="flex justify-between items-center mb-6 pb-4 border-b-2 border-purple-500/30">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-700 rounded-2xl flex items-center justify-center shadow-lg">
<span className="text-2xl">🏆</span>
</div>
<div>
<h2 className={`${isMobile ? 'text-2xl' : 'text-4xl'} font-black bg-gradient-to-r from-purple-400 to-pink-500 bg-clip-text text-transparent`}>
Achievements
</h2>
<p className="text-sm text-gray-400">
{unlockedCount}/{totalCount} Unlocked ({completionPercentage}%)
</p>
</div>
</div>
<button
onClick={onClose}
className={`${isMobile ? 'w-10 h-10' : 'w-12 h-12'} rounded-xl bg-gray-700 hover:bg-gray-600 flex items-center justify-center transition-all text-gray-300 hover:text-white text-2xl font-bold shadow-lg`}
>
×
</button>
</div>

{/* Progress Bar */}
<div className="mb-6">
<div className="w-full bg-gray-700 rounded-full h-4 overflow-hidden">
<div
className="bg-gradient-to-r from-purple-500 to-pink-500 h-full transition-all duration-500"
style={{ width: `${completionPercentage}%` }}
/>
</div>
</div>

{/* Content Container */}
<div className="overflow-y-auto h-full pb-20">
<div className={`${isMobile ? 'space-y-3' : 'grid grid-cols-1 md:grid-cols-2 gap-4'}`}>
{achievements.map(achievement => {
const progressPercent = (achievement.progress / achievement.maxProgress) * 100;
return (
<div
key={achievement.id}
className={`relative ${
isMobile ? 'p-4' : 'p-5'
} bg-gradient-to-br from-gray-800/80 to-gray-900/80 rounded-2xl shadow-lg border-2 ${
achievement.unlocked
? 'border-yellow-400/50 bg-gradient-to-br from-yellow-900/20 to-amber-900/20'
: 'border-gray-600/30'
} transition-all`}
>
{achievement.unlocked && (
<div className="absolute top-2 right-2">
<span className="text-2xl">✅</span>
</div>
)}
<div className="flex items-start gap-4 mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${categoryColors[achievement.category]} rounded-xl flex items-center justify-center text-2xl ${
!achievement.unlocked ? 'opacity-50' : ''
}`}>
{achievement.icon}
</div>
<div className="flex-grow">
<h3 className={`${isMobile ? 'text-base' : 'text-lg'} font-bold text-white mb-1 ${
!achievement.unlocked ? 'opacity-70' : ''
}`}>
{achievement.name}
</h3>
<p className={`${isMobile ? 'text-xs' : 'text-sm'} text-gray-400 ${
!achievement.unlocked ? 'opacity-70' : ''
}`}>
{achievement.description}
</p>
</div>
</div>

{!achievement.unlocked && (
<>
<div className="w-full bg-gray-700 rounded-full h-2 mb-2 overflow-hidden">
<div
className="bg-gradient-to-r from-blue-500 to-cyan-500 h-full transition-all duration-500"
style={{ width: `${Math.min(progressPercent, 100)}%` }}
/>
</div>
<p className="text-xs text-gray-500">
Progress: {abbreviateNumber(achievement.progress)}/{abbreviateNumber(achievement.maxProgress)}
</p>
</>
)}

<div className={`mt-3 pt-3 border-t border-gray-700 ${isMobile ? 'text-xs' : 'text-sm'} text-green-400 font-semibold`}>
🎁 Reward: {achievement.reward}
</div>
</div>
);
})}
</div>
</div>

{/* Footer */}
<div className={`absolute bottom-0 left-0 right-0 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 border-t-2 border-purple-500/30 ${
isMobile ? 'p-4' : 'p-5'
} backdrop-blur-md`}>
<button
onClick={onClose}
className={`${
isMobile ? 'px-5 py-2.5 text-sm' : 'px-6 py-3 text-base'
} bg-gradient-to-r from-gray-600 to-gray-700 text-white font-bold rounded-xl hover:from-gray-700 hover:to-gray-800 transition-all shadow-lg w-full`}
>
Close
</button>
</div>
</div>
</div>
);
};

export default AchievementsModal;
57 changes: 42 additions & 15 deletions app/components/CookieClickerGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,46 @@ interface CookieClickerGameProps {
setPopup: Dispatch<SetStateAction<{ title: string; message: string; onConfirm: () => 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({
clicks, setClicks, cash, setCash, clickPower, setClickPower,
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);

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) => {
Expand All @@ -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' });
Expand All @@ -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' });
Expand All @@ -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 (
<div className="flex flex-col items-center justify-center flex-grow p-3 sm:p-6 md:p-8 min-h-screen" onContextMenu={(e) => e.preventDefault()}>
Expand Down Expand Up @@ -193,19 +220,19 @@ export default function CookieClickerGame({
<div className="mt-6 sm:mt-8 grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 md:gap-4 w-full max-w-xs sm:max-w-3xl px-2 sm:px-0">
<div className="bg-gradient-to-br from-blue-500/10 to-blue-600/10 border border-blue-500/30 rounded-xl p-2 sm:p-3 backdrop-blur-sm">
<p className="text-xs text-blue-300 font-medium mb-1">Click Power</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">{abbreviateNumber(Math.floor(clickPower * prestigeMultiplier))}</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">{abbreviateNumber(Math.floor(clickPower * prestigeMultiplier * clickSkillMultiplier))}</p>
</div>
<div className="bg-gradient-to-br from-green-500/10 to-green-600/10 border border-green-500/30 rounded-xl p-2 sm:p-3 backdrop-blur-sm">
<p className="text-xs text-green-300 font-medium mb-1">Auto Clickers</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">{abbreviateNumber(Math.floor(autoClickers * prestigeMultiplier))}/s</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">{abbreviateNumber(Math.floor(autoClickers * prestigeMultiplier * autoClickerSkillMultiplier))}/s</p>
</div>
<div className="bg-gradient-to-br from-purple-500/10 to-purple-600/10 border border-purple-500/30 rounded-xl p-2 sm:p-3 backdrop-blur-sm">
<p className="text-xs text-purple-300 font-medium mb-1">Conversion Rate</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">{((0.1 + (rebirths * 0.01)) * 100).toFixed(0)}%</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">{(((0.1 + (rebirths * 0.01)) + skillModifiers.conversionRateBonus) * 100).toFixed(0)}%</p>
</div>
<div className="bg-gradient-to-br from-yellow-500/10 to-yellow-600/10 border border-yellow-500/30 rounded-xl p-2 sm:p-3 backdrop-blur-sm">
<p className="text-xs text-yellow-300 font-medium mb-1">Crate Cost</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">${abbreviateNumber(luckyCrateCost)}</p>
<p className="text-base sm:text-lg md:text-xl font-bold text-white">${abbreviateNumber(effectiveLuckyCrateCost)}</p>
</div>
</div>

Expand All @@ -221,7 +248,7 @@ export default function CookieClickerGame({
cash={cash}
upgrades={upgrades}
onPurchaseUpgrade={purchaseUpgrade}
luckyCrateCost={luckyCrateCost}
luckyCrateCost={effectiveLuckyCrateCost}
onPurchaseLuckyCrate={purchaseLuckyCrate}
/>
</div>
Expand Down
Loading