diff --git a/README.md b/README.md index 6591339..6c11775 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Open `src/data/characters.json` and add a new object to the array: ```json { "name": "Character Name", - "baseMultiplier": 5, + "iq": 100, "status": "Alive", "species": "Human", "avatarId": 1 @@ -60,7 +60,7 @@ Open `src/data/characters.json` and add a new object to the array: ``` - **name**: Display name of the character. -- **baseMultiplier**: Base Mega Seeds per second (integer or float). +- **iq**: Intelligence Quotient (1-400), used in Mega Seeds production formula: `floor(iq^1.5 * rarity_multiplier * 0.1)`. - **status**: Character status (`"Alive"`, `"Dead"`, or `"Unknown"`). - **species**: The species of the character. - **avatarId**: The ID used for the character's image. diff --git a/src/App.tsx b/src/App.tsx index d79452a..96c921c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import Packs from "./pages/Packs"; import Settings from "./pages/Settings"; import Dimension from "./pages/Dimension"; import Upgrades from "./pages/Upgrades"; +import Splicer from "./pages/Splicer"; import NotFound from "./pages/NotFound"; import { useEffect, useRef } from "react"; @@ -18,6 +19,7 @@ import { formatCurrency } from "@/lib/utils"; import { GAME_CONFIG } from "@/config/gameConfig"; import { initGA, trackPageView } from "@/lib/analytics"; import { useLocation } from "react-router-dom"; +import { AutoOpenManager } from "@/components/game/AutoOpenManager"; const queryClient = new QueryClient(); @@ -80,6 +82,7 @@ const App = () => { + @@ -89,6 +92,7 @@ const App = () => { } /> } /> } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/game/AutoOpenManager.tsx b/src/components/game/AutoOpenManager.tsx new file mode 100644 index 0000000..6eebbe2 --- /dev/null +++ b/src/components/game/AutoOpenManager.tsx @@ -0,0 +1,152 @@ +import { useEffect, useCallback, useRef } from "react"; +import { useGameStore, resolveCardStats } from "@/store/gameStore"; +import { Button } from "@/components/ui/button"; +import { X, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { formatCurrency } from "@/lib/utils"; +import packs from "@/data/packs.json"; + +export function AutoOpenManager() { + const isAutoOpenActive = useGameStore((s) => s.isAutoOpenActive); + const activePackId = useGameStore((s) => s.activePackId); + const autoOpenHistory = useGameStore((s) => s.autoOpenHistory); + const stopAutoOpen = useGameStore((s) => s.stopAutoOpen); + const addToAutoOpenHistory = useGameStore((s) => s.addToAutoOpenHistory); + + const buyPack = useGameStore((s) => s.buyPack); + const addCards = useGameStore((s) => s.addCards); + const inventory = useGameStore((s) => s.inventory); + const maxInventory = useGameStore((s) => s.maxInventory); + + const autoOpenTimerRef = useRef(null); + + const performAutoOpen = useCallback(() => { + if (!activePackId) return; + + const pack = packs.find((p) => p.id === activePackId); + if (!pack) { + stopAutoOpen(); + return; + } + + const state = useGameStore.getState(); + + // Check conditions + if (state.seeds < pack.cost) { + toast.error("Out of Mega Seeds!", { description: "Auto-open stopped." }); + stopAutoOpen(); + return; + } + + if (state.inventory.length >= state.maxInventory) { + toast.error("Inventory Full!", { description: "Auto-open stopped." }); + stopAutoOpen(); + return; + } + + const newCards = buyPack(activePackId); + if (newCards) { + addCards(newCards); + addToAutoOpenHistory(newCards); + + // Schedule next open - slowed down to 1.2s for better visibility + autoOpenTimerRef.current = setTimeout(performAutoOpen, 1200); + } else { + stopAutoOpen(); + } + }, [activePackId, buyPack, addCards, stopAutoOpen, addToAutoOpenHistory]); + + useEffect(() => { + if (isAutoOpenActive) { + // Start the loop if not already running + if (!autoOpenTimerRef.current) { + performAutoOpen(); + } + } else { + // Clear timer if stopped + if (autoOpenTimerRef.current) { + clearTimeout(autoOpenTimerRef.current); + autoOpenTimerRef.current = null; + } + } + + return () => { + if (autoOpenTimerRef.current) { + clearTimeout(autoOpenTimerRef.current); + autoOpenTimerRef.current = null; + } + }; + }, [isAutoOpenActive, performAutoOpen]); + + if (!isAutoOpenActive || !activePackId) return null; + + const activePack = packs.find(p => p.id === activePackId); + if (!activePack) return null; + + return ( +
+
+
+
+ + Auto-Opening +
+ +
+ +
+
+ {activePack.name} + {formatCurrency(activePack.cost)} +
+ +
+ {autoOpenHistory.length === 0 && ( +

Stabilizing portal...

+ )} + {autoOpenHistory.map((card, i) => { + const stats = resolveCardStats(card); + return ( +
+ +
+
+

{stats.character.name}

+ IQ {stats.character.iq} +
+
+ {card.types.map(t => ( + {t} + ))} +
+
+
+

+{stats.income}

+
+
+ ); + })} +
+ +
+
+ Inventory: + = maxInventory ? "text-red-500" : "text-primary"}> + {inventory.length}/{maxInventory} + +
+ +
+
+
+
+ ); +} diff --git a/src/components/game/GameCard.tsx b/src/components/game/GameCard.tsx index 68df8f2..6320946 100644 --- a/src/components/game/GameCard.tsx +++ b/src/components/game/GameCard.tsx @@ -54,7 +54,7 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {

- +
diff --git a/src/components/game/Header.tsx b/src/components/game/Header.tsx index ef41138..17ae672 100644 --- a/src/components/game/Header.tsx +++ b/src/components/game/Header.tsx @@ -8,16 +8,18 @@ import { GAME_CONFIG } from '@/config/gameConfig'; export function Header() { const seeds = useGameStore((s) => s.seeds); const activeSlots = useGameStore((s) => s.activeSlots); - const inventory = useGameStore((s) => s.inventory); + const discoveredCards = useGameStore((s) => s.discoveredCards); const upgrades = useGameStore((s) => s.upgrades); - const activeCount = activeSlots.filter(Boolean).length; - const inactiveCards = Math.max(0, inventory.length - activeCount); + const totalDiscoveries = Object.values(discoveredCards).reduce( + (sum, types) => sum + types.length, + 0, + ); - const collectionBonus = Math.round(inactiveCards * GAME_CONFIG.INCOME.INACTIVE_CARD_BONUS * 100); + const collectionBonus = Math.round(totalDiscoveries * GAME_CONFIG.INCOME.INACTIVE_CARD_BONUS * 100); const labBonus = Math.round((upgrades.seeds || 0) * GAME_CONFIG.UPGRADES.seeds.BONUS_PER_LEVEL * 100); - const totalIncome = calculateCurrentIncome({ activeSlots, inventory, upgrades }); + const totalIncome = calculateCurrentIncome({ activeSlots, discoveredCards, upgrades }); return (
diff --git a/src/components/game/PackOpening.tsx b/src/components/game/PackOpening.tsx index c66721b..9432c90 100644 --- a/src/components/game/PackOpening.tsx +++ b/src/components/game/PackOpening.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { useGameStore, resolveCardStats } from "@/store/gameStore"; import { GameCard } from "./GameCard"; import { Button } from "@/components/ui/button"; -import { Package, Sparkles, X, ChevronRight, Lock } from "lucide-react"; +import { Package, Sparkles, X, ChevronRight, Lock, Play, Pause, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { formatCurrency } from "@/lib/utils"; import packs from "@/data/packs.json"; @@ -31,6 +31,14 @@ export function PackOpening({ packId }: PackOpeningProps) { const [addedToInventory, setAddedToInventory] = useState(false); const [portalVibration, setPortalVibration] = useState(0); + // Global Auto-open state + const isGlobalAutoOpenActive = useGameStore((s) => s.isAutoOpenActive); + const activePackId = useGameStore((s) => s.activePackId); + const startAutoOpen = useGameStore((s) => s.startAutoOpen); + const stopAutoOpen = useGameStore((s) => s.stopAutoOpen); + + const isThisPackAutoOpening = isGlobalAutoOpenActive && activePackId === packId; + const sellCard = useGameStore((s) => s.sellCard); useEffect(() => { @@ -64,6 +72,9 @@ export function PackOpening({ packId }: PackOpeningProps) { return; } + // Stop global auto-open if manual opening starts for THIS pack + if (isGlobalAutoOpenActive) stopAutoOpen(); + // If we're already in a portal (Open Again), add current cards to inventory first if not already added if (showCards.length > 0 && !addedToInventory) { const remainingCards = revealedCards.filter( @@ -87,6 +98,22 @@ export function PackOpening({ packId }: PackOpeningProps) { } }; + const handleToggleAutoOpen = () => { + if (isThisPackAutoOpening) { + stopAutoOpen(); + } else { + if (seeds < pack.cost) { + toast.error("Not enough Mega Seeds!"); + return; + } + if (inventory.length >= maxInventory) { + toast.error("Inventory Full!"); + return; + } + startAutoOpen(packId); + } + }; + const revealCard = (index: number) => { if (showCards.includes(index)) return; const newShowCards = [...showCards, index]; @@ -211,14 +238,30 @@ export function PackOpening({ packId }: PackOpeningProps) {
- +
+ + {isUnlocked && ( + + )} +
{isOpen && (
@@ -320,7 +363,7 @@ export function PackOpening({ packId }: PackOpeningProps) { onClick={() => revealCard(i)} className="w-44 h-64 bg-gradient-to-br from-muted/80 to-muted-foreground/10 rounded-2xl border-2 border-border/50 flex flex-col items-center justify-center gap-4 hover:border-primary/50 transition-all hover:scale-105 group relative overflow-hidden shadow-2xl" > -
+
diff --git a/src/components/game/PortalArea.tsx b/src/components/game/PortalArea.tsx index fd8ea96..76c8f79 100644 --- a/src/components/game/PortalArea.tsx +++ b/src/components/game/PortalArea.tsx @@ -8,7 +8,7 @@ export function PortalArea() { const toggleSlot = useGameStore((s) => s.toggleSlot); const activeCount = activeSlots.filter(Boolean).length; - const inactiveCards = Math.max(0, (inventory.length - activeCount)); + const inactiveCards = Math.max(0, inventory.length - activeCount); return (
@@ -17,11 +17,7 @@ export function PortalArea() { Portal Slots

- Place cards to generate Mega Seeds • Collection bonus:{" "} - - +{Math.max(0, Math.floor((inventory.length - activeCount) / 2))}% - {" "} - ({inactiveCards} cards in inventory) + Place cards to generate Mega Seeds

diff --git a/src/components/game/card-parts/CardStats.tsx b/src/components/game/card-parts/CardStats.tsx index d41f73f..a3adde3 100644 --- a/src/components/game/card-parts/CardStats.tsx +++ b/src/components/game/card-parts/CardStats.tsx @@ -1,12 +1,13 @@ -import { Leaf, Sword } from "lucide-react"; +import { Leaf, Sword, Brain } from "lucide-react"; import { formatNumber } from "@/lib/utils"; interface CardStatsProps { income: number; power: number; + iq?: number; } -export function CardStats({ income, power }: CardStatsProps) { +export function CardStats({ income, power, iq }: CardStatsProps) { return (
@@ -21,6 +22,12 @@ export function CardStats({ income, power }: CardStatsProps) { {formatNumber(power)}
+ {iq !== undefined && ( +
+ + {iq} +
+ )}
); } diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx index f9f131e..007363a 100644 --- a/src/components/ui/progress.tsx +++ b/src/components/ui/progress.tsx @@ -9,11 +9,14 @@ const Progress = React.forwardRef< >(({ className, value, ...props }, ref) => ( diff --git a/src/config/gameConfig.ts b/src/config/gameConfig.ts index ee57965..70b0bbf 100644 --- a/src/config/gameConfig.ts +++ b/src/config/gameConfig.ts @@ -54,6 +54,7 @@ export const GAME_CONFIG = { DIMENSIONS: { BONUS_STEP: 5, BONUS_AMOUNT: 200, // (dimensionLevel + 1) * 200 + INVENTORY_BONUS_PER_STEP: 5, // +5 slots every 5 levels SCALE_FACTOR: 0.25, MAX_LEVEL: 100, MAX_LEVEL_REWARD: 1000000, diff --git a/src/data/cardTypes.json b/src/data/cardTypes.json index 7437fb7..15efd04 100644 --- a/src/data/cardTypes.json +++ b/src/data/cardTypes.json @@ -14,35 +14,35 @@ { "id": "HOLO", "label": "Holo", - "multiplier": 3, + "multiplier": 4, "canCombine": true, "combinesWith": ["FULL_ART"] }, { "id": "FULL_ART", "label": "Full Art", - "multiplier": 4, + "multiplier": 8, "canCombine": true, "combinesWith": ["HOLO", "SILVER", "GOLD", "REVERT"] }, { "id": "SILVER", "label": "Silver", - "multiplier": 6, + "multiplier": 10, "canCombine": true, "combinesWith": ["FULL_ART"] }, { "id": "GOLD", "label": "Gold", - "multiplier": 10, + "multiplier": 12, "canCombine": true, "combinesWith": ["FULL_ART"] }, { "id": "REVERT", "label": "Revert", - "multiplier": 15, + "multiplier": 20, "canCombine": true, "combinesWith": ["FULL_ART"] } diff --git a/src/data/characters.json b/src/data/characters.json index 947c3e1..0a981a2 100644 --- a/src/data/characters.json +++ b/src/data/characters.json @@ -10,7 +10,7 @@ "location": "Citadel of Ricks", "customImage": "https://rickandmortyapi.com/api/character/avatar/1.jpeg", "avatarId": 1, - "baseMultiplier": 1000, + "iq": 500, "basePower": 500 }, { @@ -23,7 +23,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 2, - "baseMultiplier": 25, + "iq": 90, "basePower": 150 }, { @@ -36,7 +36,7 @@ "origin": "Earth (Replacement Dimension)", "location": "Earth (Replacement Dimension)", "avatarId": 3, - "baseMultiplier": 45, + "iq": 115, "basePower": 100 }, { @@ -49,7 +49,7 @@ "origin": "Earth (Replacement Dimension)", "location": "Earth (Replacement Dimension)", "avatarId": 4, - "baseMultiplier": 50, + "iq": 155, "basePower": 15 }, { @@ -62,7 +62,7 @@ "origin": "Earth (Replacement Dimension)", "location": "Earth (Replacement Dimension)", "avatarId": 5, - "baseMultiplier": 150, + "iq": 20, "basePower": 1 }, { @@ -75,7 +75,7 @@ "origin": "Abadango", "location": "Abadango", "avatarId": 6, - "baseMultiplier": 85, + "iq": 130, "basePower": 45 }, { @@ -88,7 +88,7 @@ "origin": "Earth (Replacement Dimension)", "location": "Testicle Monster Dimension", "avatarId": 7, - "baseMultiplier": 500, + "iq": 120, "basePower": 66 }, { @@ -101,7 +101,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 8, - "baseMultiplier": 250, + "iq": 245, "basePower": 450 }, { @@ -114,7 +114,7 @@ "origin": "Earth (Replacement Dimension)", "location": "Earth (Replacement Dimension)", "avatarId": 9, - "baseMultiplier": 1500, + "iq": 230, "basePower": 5 }, { @@ -127,7 +127,7 @@ "origin": "unknown", "location": "Worldender's lair", "avatarId": 10, - "baseMultiplier": 100, + "iq": 110, "basePower": 300 }, { @@ -140,7 +140,7 @@ "origin": "Earth (C-137)", "location": "Earth (Replacement Dimension)", "avatarId": 11, - "baseMultiplier": 5000, + "iq": 400, "basePower": 0 }, { @@ -153,7 +153,7 @@ "origin": "Earth (C-137)", "location": "Anatomy Park", "avatarId": 12, - "baseMultiplier": 12, + "iq": 70, "basePower": 5 }, { @@ -166,7 +166,7 @@ "origin": "unknown", "location": "Earth (Replacement Dimension)", "avatarId": 13, - "baseMultiplier": 50, + "iq": 60, "basePower": 50 }, { @@ -179,7 +179,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 14, - "baseMultiplier": 60, + "iq": 85, "basePower": 125 }, { @@ -192,7 +192,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 15, - "baseMultiplier": 500, + "iq": 360, "basePower": 400 }, { @@ -205,7 +205,7 @@ "origin": "unknown", "location": "Earth (Replacement Dimension)", "avatarId": 16, - "baseMultiplier": 75, + "iq": 90, "basePower": 80 }, { @@ -218,7 +218,7 @@ "origin": "Earth (C-137)", "location": "Anatomy Park", "avatarId": 17, - "baseMultiplier": 40, + "iq": 100, "basePower": 20 }, { @@ -231,7 +231,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 18, - "baseMultiplier": 65, + "iq": 88, "basePower": 135 }, { @@ -245,7 +245,7 @@ "location": "unknown", "customImage": "https://static.wikia.nocookie.net/rickandmorty/images/4/49/Antenna_Rick.png/revision/latest?cb=20161121231006", "avatarId": 19, - "baseMultiplier": 450, + "iq": 365, "basePower": 350 }, { @@ -258,7 +258,7 @@ "origin": "unknown", "location": "Interdimensional Cable", "avatarId": 20, - "baseMultiplier": 120, + "iq": 50, "basePower": 10 }, { @@ -271,7 +271,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 21, - "baseMultiplier": 40, + "iq": 82, "basePower": 110 }, { @@ -284,7 +284,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 22, - "baseMultiplier": 600, + "iq": 370, "basePower": 450 }, { @@ -297,7 +297,7 @@ "origin": "unknown", "location": "Immortality Field Resort", "avatarId": 23, - "baseMultiplier": 120, + "iq": 80, "basePower": 50 }, { @@ -310,7 +310,7 @@ "origin": "Signus 5 Expanse", "location": "Signus 5 Expanse", "avatarId": 24, - "baseMultiplier": 2000, + "iq": 330, "basePower": 10 }, { @@ -323,7 +323,7 @@ "origin": "Post-Apocalyptic Earth", "location": "Post-Apocalyptic Earth", "avatarId": 25, - "baseMultiplier": 80, + "iq": 65, "basePower": 250 }, { @@ -336,7 +336,7 @@ "origin": "Purge Planet", "location": "Purge Planet", "avatarId": 26, - "baseMultiplier": 90, + "iq": 110, "basePower": 180 }, { @@ -349,7 +349,7 @@ "origin": "unknown", "location": "Citadel of Ricks", "avatarId": 27, - "baseMultiplier": 50, + "iq": 95, "basePower": 95 }, { @@ -362,7 +362,7 @@ "origin": "unknown", "location": "Interdimensional Cable", "avatarId": 28, - "baseMultiplier": 150, + "iq": 95, "basePower": 300 }, { @@ -375,7 +375,7 @@ "origin": "unknown", "location": "Interdimensional Cable", "avatarId": 29, - "baseMultiplier": 100, + "iq": 90, "basePower": 40 }, { @@ -388,7 +388,7 @@ "origin": "unknown", "location": "unknown", "avatarId": 30, - "baseMultiplier": 250, + "iq": 140, "basePower": 50 }, { @@ -401,7 +401,7 @@ "origin": "unknown", "location": "Earth (Replacement Dimension)", "avatarId": 31, - "baseMultiplier": 300, + "iq": 180, "basePower": 150 }, { @@ -414,7 +414,7 @@ "origin": "unknown", "location": "Earth (Replacement Dimension)", "avatarId": 32, - "baseMultiplier": 75, + "iq": 80, "basePower": 45 }, { @@ -427,7 +427,7 @@ "origin": "Venzenulon 7", "location": "Venzenulon 7", "avatarId": 33, - "baseMultiplier": 40, + "iq": 55, "basePower": 20 }, { @@ -440,7 +440,7 @@ "origin": "unknown", "location": "Interdimensional Cable", "avatarId": 34, - "baseMultiplier": 120, + "iq": 145, "basePower": 60 }, { @@ -453,7 +453,7 @@ "origin": "Bepis 9", "location": "Bepis 9", "avatarId": 35, - "baseMultiplier": 85, + "iq": 75, "basePower": 35 }, { @@ -466,7 +466,7 @@ "origin": "unknown", "location": "unknown", "avatarId": 36, - "baseMultiplier": 1500, + "iq": 320, "basePower": 200 }, { @@ -479,7 +479,7 @@ "origin": "Earth (C-500A)", "location": "Earth (C-500A)", "avatarId": 37, - "baseMultiplier": 55, + "iq": 158, "basePower": 20 }, { @@ -492,7 +492,7 @@ "origin": "Earth (C-137)", "location": "Earth (C-137)", "avatarId": 38, - "baseMultiplier": 50, + "iq": 160, "basePower": 15 }, { @@ -505,7 +505,7 @@ "origin": "Earth (Evil Rick's Target Dimension)", "location": "Earth (Evil Rick's Target Dimension)", "avatarId": 39, - "baseMultiplier": 50, + "iq": 150, "basePower": 15 }, { @@ -518,7 +518,7 @@ "origin": "Nuptia 4", "location": "Nuptia 4", "avatarId": 40, - "baseMultiplier": 400, + "iq": 190, "basePower": 250 } ] diff --git a/src/index.css b/src/index.css index 3febdf3..4246869 100644 --- a/src/index.css +++ b/src/index.css @@ -126,10 +126,36 @@ transform: translateY(0); } 50% { - transform: translateY(-30px); + transform: translateY(-40px) scale(1.05); } } +@keyframes damage-popup { + 0% { + opacity: 0; + transform: translateY(0) scale(0.5); + } + 20% { + opacity: 1; + transform: translateY(-20px) scale(1.4); + } + 80% { + opacity: 1; + transform: translateY(-40px) scale(1); + } + 100% { + opacity: 0; + transform: translateY(-60px) scale(0.8); + } +} + +.animate-damage { + animation: damage-popup 1s cubic-bezier(0.17, 0.67, 0.83, 0.67) forwards; + text-shadow: 0px 0px 6px rgba(0, 0, 0, 1); + font-weight: 900; + letter-spacing: -0.05em; +} + .animate-holo { background: linear-gradient( 135deg, @@ -158,9 +184,19 @@ } .animate-battle-jump { - animation: battleJump 1.5s ease-in-out infinite; + animation: battleJump 0.4s ease-out; } .discord-color { color: #5865f2; } + +@layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/src/pages/Collection.tsx b/src/pages/Collection.tsx index af10a9e..dfc568b 100644 --- a/src/pages/Collection.tsx +++ b/src/pages/Collection.tsx @@ -22,16 +22,23 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Search, Trash2, FilterX, CheckCircle2, Circle, ArrowLeft } from 'lucide-react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Progress } from "@/components/ui/progress"; +import { Search, Trash2, FilterX, CheckCircle2, Circle, ArrowLeft, BookOpen, LayoutGrid, Info } from 'lucide-react'; import { toast } from 'sonner'; import { Link } from 'react-router-dom'; import { GAME_CONFIG } from '@/config/gameConfig'; +import charactersData from '@/data/characters.json'; +import cardTypesData from '@/data/cardTypes.json'; +import { TYPE_ICONS } from '@/config/rarityConfig'; const Collection = () => { const inventory = useGameStore((s) => s.inventory); const maxInventory = useGameStore((s) => s.maxInventory); const sellCards = useGameStore((s) => s.sellCards); + const discoveredCards = useGameStore((s) => s.discoveredCards); + const [activeTab, setActiveTab] = useState('inventory'); const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState('ALL'); const [sortBy, setSortBy] = useState('newest'); @@ -45,7 +52,7 @@ const Collection = () => { return inventory .filter(Boolean) .map((card) => ({ card, stats: resolveCardStats(card) })) - .filter(({ stats }) => { + .filter(({ card, stats }) => { const matchesSearch = stats.character.name .toLowerCase() .includes(search.toLowerCase()); @@ -57,10 +64,41 @@ const Collection = () => { if (sortBy === "newest") return b.card.timestamp - a.card.timestamp; if (sortBy === "oldest") return a.card.timestamp - b.card.timestamp; if (sortBy === "income") return b.stats.income - a.stats.income; + if (sortBy === "iq") return b.stats.character.iq - a.stats.character.iq; + if (sortBy === "power") return b.stats.power - a.stats.power; return 0; }); }, [inventory, search, typeFilter, sortBy]); + const pokedexStats = useMemo(() => { + const totalCharacters = charactersData.length; + const discoveredCount = Object.keys(discoveredCards).length; + const percentage = Math.floor((discoveredCount / totalCharacters) * 100); + + // Calculate total rarities discovered across all characters + const totalRaritiesPossible = totalCharacters * cardTypesData.length; + const totalRaritiesDiscovered = Object.values(discoveredCards).reduce((acc, types) => acc + (types as string[]).length, 0); + + // Using 1 decimal place for rarity percentage since it grows slowly + const rarityPercentage = totalRaritiesPossible > 0 + ? parseFloat(((totalRaritiesDiscovered / totalRaritiesPossible) * 100).toFixed(1)) + : 0; + + return { totalCharacters, discoveredCount, percentage, rarityPercentage }; + }, [discoveredCards]); + + const filteredPokedex = useMemo(() => { + return charactersData + .filter(char => char.name.toLowerCase().includes(search.toLowerCase())) + .sort((a, b) => { + const aDiscovered = !!discoveredCards[a.id]; + const bDiscovered = !!discoveredCards[b.id]; + if (aDiscovered && !bDiscovered) return -1; + if (!aDiscovered && bDiscovered) return 1; + return a.id - b.id; + }); + }, [search, discoveredCards]); + const toggleSelect = (id: string) => { setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] @@ -100,7 +138,7 @@ const Collection = () => {
-
+
-

Your Collection

-

{inventory.length} / {maxInventory} cards collected

+

Citadel Archives

+
+

Inventory: {inventory.length} / {maxInventory}

+

Registry: {pokedexStats.discoveredCount} / {pokedexStats.totalCharacters}

+
-
- -
+ + + + + + Inventory + + + + Archives + + +
+ {activeTab === 'archives' && ( +
+
+
+
+ + + Dimensional Discovery + + {pokedexStats.percentage}% +
+ +
+
+
+
+ + + Rarity Resonance + + {pokedexStats.rarityPercentage}% +
+ +
+
+
+ )} + {/* Filters */}
setSearch(e.target.value)} />
- + {activeTab === 'inventory' ? ( + <> + - + + + ) : ( +
+ + Discovered characters appear in color. Collect all rarities for 100% completion! +
+ )}
- {/* Results */} - {filteredCards.length > 0 ? ( -
- {filteredCards.map(({ card }) => { - const isSelected = selectedIds.includes(card.id); - return ( -
isSellMode && toggleSelect(card.id)} - > - {isSellMode && ( -
- {isSelected ? ( - - ) : ( - + + +
+ +
+ + {filteredCards.length > 0 ? ( +
+ {filteredCards.map(({ card }) => { + const isSelected = selectedIds.includes(card.id); + return ( +
isSellMode && toggleSelect(card.id)} + > + {isSellMode && ( +
+ {isSelected ? ( + + ) : ( + + )} +
)} +
+ +
- )} -
- -
+ ); + })} +
+ ) : ( +
+
+
- ); - })} -
- ) : ( -
-
- -
-
-

No cards found

-

Try adjusting your filters or search terms.

+
+

No cards found

+

Try adjusting your filters or search terms.

+
+ +
+ )} + + + +
+ {filteredPokedex.map((char) => { + const typesDiscovered = discoveredCards[char.id] || []; + const isDiscovered = typesDiscovered.length > 0; + + return ( +
+
+ {char.name} + {!isDiscovered && ( +
+ Registry Locked +
+ )} +
+ +
+
+

+ {isDiscovered ? char.name : "???"} +

+ {isDiscovered && ( + + IQ {char.iq} + + )} +
+ + {/* Rarity Indicators */} +
+ {cardTypesData.map((type) => { + const Icon = TYPE_ICONS[type.id] || Circle; + const found = typesDiscovered.includes(type.id); + return ( +
+ +
+ ); + })} +
+ +
+ + {isDiscovered ? char.species : "unknown"} + + + #{char.id.toString().padStart(3, '0')} + +
+
+
+ ); + })}
- -
- )} +
+
{/* Sell Mode Floating Bar */} - {isSellMode && ( -
-
-
+ {isSellMode && activeTab === 'inventory' && ( +
+
+

{selectedIds.length} Selected

Total: {Math.floor(selectedTotalProfit).toLocaleString()} Seeds

-
+
+ + + + +
+
+
+

+ + Rift Completion +

+

+ Unlock powerful bonuses at each milestone +

+
+
+

+ {maxDimensionLevel} + + /100 + +

+
+
+ +
+
+ {/* Milestone Markers */} + {[10, 25, 50, 100].map((m) => ( +
= m ? "bg-white/40" : "bg-white/10"}`} + style={{ left: `${m}%` }} + /> + ))} +
+ +
+ {[10, 25, 50, 100].map((m) => { + const name = GAME_CONFIG.DIMENSIONS.MILESTONES[m]; + const isReached = maxDimensionLevel >= m; + return ( +
+
+ + {m} + +
+

+ {name || "???"} +

+
+ {isReached ? "UNLOCKED" : "LOCKED"} +
+
+ ); + })} +
+ +
+
+ +
+
+

+ Current Active Bonus +

+

+ You've gained{" "} + + +{Math.floor(maxDimensionLevel / 5) * 5} + {" "} + extra card slots from your rift progression. +

+
+
+
+ + + {!isDimensionActive ? (
@@ -213,16 +436,27 @@ const Dimension = () => {
) : (
-
+
{/* PLAYER */} -
+
You
{playerStats && } + + {/* Player Damage Indicator */} + {battleState.playerDamage !== null && ( +
+ + -{formatNumber(battleState.playerDamage)} + +
+ )}
@@ -238,8 +472,9 @@ const Dimension = () => {
-
-
+ {/* VS ICON */} +
+
VS @@ -247,14 +482,25 @@ const Dimension = () => {
{/* ENEMY */} -
+
Enemy
{currentEnemy && } + + {/* Enemy Damage Indicator */} + {battleState.enemyDamage !== null && ( +
+ + -{formatNumber(battleState.enemyDamage)} + +
+ )}
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 5dc0c28..5e69b23 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -4,7 +4,7 @@ import { CollectionTab } from "@/components/game/CollectionTab"; import { Footer } from "@/components/game/Footer"; import { Link } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { Package, Map, Beaker } from "lucide-react"; +import { Package, Map, Beaker, Dna } from "lucide-react"; const Index = () => { return ( @@ -15,20 +15,31 @@ const Index = () => {
+ + + diff --git a/src/pages/Splicer.tsx b/src/pages/Splicer.tsx new file mode 100644 index 0000000..47d857b --- /dev/null +++ b/src/pages/Splicer.tsx @@ -0,0 +1,431 @@ +import { useState, useMemo } from "react"; +import { useGameStore, resolveCardStats } from "@/store/gameStore"; +import { Header } from "@/components/game/Header"; +import { GameCard } from "@/components/game/GameCard"; +import { Footer } from "@/components/game/Footer"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { + ArrowLeft, + Beaker, + Sparkles, + AlertTriangle, + Zap, + ChevronRight, + Dna, + Filter, + Trash2, + Info, + Loader2, +} from "lucide-react"; +import { Link } from "react-router-dom"; +import { + getPrimaryType, + RARITY_STYLES, + TYPE_ICONS, +} from "@/config/rarityConfig"; +import { toast } from "sonner"; +import cardTypes from "@/data/cardTypes.json"; + +const Splicer = () => { + const inventory = useGameStore((s) => s.inventory); + const spliceCards = useGameStore((s) => s.spliceCards); + + const [selectedIds, setSelectedIds] = useState([]); + const [isSplicing, setIsSplicing] = useState(false); + const [resultCard, setResultCard] = useState(null); + const [filterRarity, setFilterRarity] = useState("COMMON"); + + const toggleSelect = (id: string) => { + if (isSplicing) return; + setResultCard(null); + + setSelectedIds((prev) => { + if (prev.includes(id)) return prev.filter((i) => i !== id); + if (prev.length >= 5) return prev; + return [...prev, id]; + }); + }; + + const handleFilterChange = (rarity: string) => { + setFilterRarity(rarity); + setSelectedIds([]); // Unselect all when switching tags + setResultCard(null); + }; + + const filteredInventory = useMemo(() => { + // 1. Group cards by characterId and rarity to find those with 5+ copies + const groups: Record = {}; + inventory.forEach((card) => { + const rarity = getPrimaryType(card.types); + const key = `${card.characterId}-${rarity}`; + groups[key] = (groups[key] || 0) + 1; + }); + + const validKeys = new Set( + Object.keys(groups).filter((key) => groups[key] >= 5), + ); + + // 2. Initial filter by rarity and copy count + let list = inventory.filter((card) => { + const rarity = getPrimaryType(card.types); + const key = `${card.characterId}-${rarity}`; + return rarity === filterRarity && validKeys.has(key); + }); + + // 3. If something is already selected, only show copies of that specific card + if (selectedIds.length > 0) { + const firstSelected = inventory.find((c) => c.id === selectedIds[0]); + if (firstSelected) { + list = list.filter( + (card) => + card.characterId === firstSelected.characterId && + getPrimaryType(card.types) === getPrimaryType(firstSelected.types), + ); + } + } + + return list; + }, [inventory, filterRarity, selectedIds]); + + const selectedCards = useMemo(() => { + return inventory.filter((c) => selectedIds.includes(c.id)); + }, [inventory, selectedIds]); + + const batchRarity = useMemo(() => { + if (selectedCards.length === 0) return null; + const rarities = selectedCards.map((c) => getPrimaryType(c.types)); + if (rarities.every((r) => r === rarities[0])) return rarities[0]; + return "MIXED"; + }, [selectedCards]); + + const isValidBatch = + selectedCards.length === 5 && + batchRarity && + batchRarity !== "MIXED" && + batchRarity !== "REVERT"; + + const handleSplice = async () => { + if (!isValidBatch) return; + + setIsSplicing(true); + setResultCard(null); + + // Aesthetic delay for Rick science + await new Promise((r) => setTimeout(r, 2000)); + + const result = spliceCards(selectedIds); + if (result) { + setResultCard(result); + setSelectedIds([]); + toast.success("GENETIC STABILIZATION COMPLETE", { + description: "A superior lifeform has been synthesized.", + }); + } + + setIsSplicing(false); + }; + + return ( +
+
+ +
+ {/* Header Section */} +
+
+ + + +
+

+ + Genetic Splicer +

+

+ Break biological boundaries. Upgrade your entities. +

+
+
+
+ +
+ {/* Main Controls & Selection (Lg: 8 cols) */} +
+ {/* Filter Bar */} +
+
+ {cardTypes.map(type => ( + + ))} +
+ +
+ + Ready + +
+ {[...Array(5)].map((_, i) => ( +
+ ))} +
+ {selectedIds.length > 0 && ( + + )} +
+
+ + {/* Inventory Grid - Using auto-fill to prevent overlapping */} +
+ {filteredInventory.map((card) => { + const isSelected = selectedIds.includes(card.id); + return ( +
toggleSelect(card.id)} + > +
+
+ +
+
+ {isSelected && ( +
+ +
+ )} +
+ ); + })} + + {filteredInventory.length === 0 && ( +
+ +

+ No entities with 5+ copies found in this resonance +

+
+ )} +
+
+ + {/* Splicing Machine (Lg: 4 cols) - Sticky on Desktop */} +
+
+ {/* Background Tech Decor */} +
+
+ +
+ {/* Result/Status Chamber */} +
+
+ +
+ {isSplicing ? ( +
+ + + SYNTHESIZING + +
+ ) : resultCard ? ( +
+ +
+ ) : ( +
+ + + Empty Chamber + +
+ )} + + {/* Orbital slots for selected cards */} + {!isSplicing && + !resultCard && + selectedCards.map((card, idx) => { + const angle = idx * (360 / 5) - 90; + const radius = 120; // Distance from center + const x = Math.cos(angle * (Math.PI / 180)) * radius; + const y = Math.sin(angle * (Math.PI / 180)) * radius; + + const stats = resolveCardStats(card); + + return ( +
+ +
+ ); + })} +
+
+ +
+
+

+ + Machine Diagnostics +

+ + {selectedIds.length > 0 ? ( +
+
+ + Resonance + + + {batchRarity} + +
+ + + {batchRarity === "MIXED" && ( +

+ INCOMPATIBLE + GENETICS +

+ )} + {batchRarity === "REVERT" && ( +

+ MAXIMUM EVOLUTION +

+ )} + {isValidBatch && ( +

+ STABLE FOR + FUSION +

+ )} +
+ ) : ( +

+ Waiting for genetic samples... +

+ )} +
+ + {resultCard ? ( + + ) : ( + + )} +
+
+
+ + {/* Evolution Helper - Desktop only or bottom mobile */} +
+
+ + Lab Protocol +
+
+ {[ + { from: "COMMON", to: "RARE" }, + { from: "RARE", to: "HOLO" }, + { from: "HOLO", to: "FULL_ART" }, + { from: "FULL_ART", to: "SILVER" }, + { from: "SILVER", to: "GOLD" }, + { from: "GOLD", to: "REVERT" }, + ].map((step, i) => ( +
+
+ + {step.from} + + + + {step.to} + +
+ + x5 + +
+ ))} +
+
+
+
+
+
+
+ ); +}; + +export default Splicer; diff --git a/src/store/cardUtils.ts b/src/store/cardUtils.ts index 1893242..35ddc86 100644 --- a/src/store/cardUtils.ts +++ b/src/store/cardUtils.ts @@ -6,10 +6,13 @@ import { GAME_CONFIG } from "@/config/gameConfig"; export const generateCard = ( weights: Record = GAME_CONFIG.CARD_GENERATION.DEFAULT_WEIGHTS, combineChance: number = GAME_CONFIG.CARD_GENERATION.DEFAULT_COMBINE_CHANCE, + characterId?: number, ): GameCard => { - const character = (characters as Character[])[ - Math.floor(Math.random() * characters.length) - ]; + const character = characterId + ? characterMap.get(characterId) + : (characters as Character[])[ + Math.floor(Math.random() * characters.length) + ]; const typeRoll = Math.random(); let baseTypeId = "COMMON"; @@ -76,12 +79,17 @@ export const resolveCardStats = (card: GameCard) => { return acc * (type?.multiplier || 1); }, 1); - const baseIncome = - card.income !== undefined ? card.income : character.baseMultiplier; const basePower = card.power !== undefined ? card.power : character.basePower; + // New formula: (iq^1.5) * rarityMultiplier / 10 + const calculatedIncome = Math.floor( + Math.pow(character.iq, 1.5) * combinedMultiplier * 0.1, + ); + + const income = card.income !== undefined ? card.income : calculatedIncome; + return { - income: Math.floor(baseIncome * combinedMultiplier), + income, power: Math.floor(basePower * combinedMultiplier), character, }; @@ -93,7 +101,7 @@ export const resolveCardStats = (card: GameCard) => { * @returns Income per second */ export const calculateCurrentIncome = ( - state: Pick, + state: Pick, ) => { const activeIncome = state.activeSlots.reduce( (sum: number, slot: GameCard | null) => { @@ -103,11 +111,13 @@ export const calculateCurrentIncome = ( 0, ); - // Optimized: Instead of filtering, use simple subtraction ($O(1)$ -> $O(N)$) - const activeCount = state.activeSlots.filter(Boolean).length; - const inactiveCards = Math.max(0, state.inventory.length - activeCount); + // Calculate total discoveries from Citadel Archives (characterId -> types[]) + const totalDiscoveries = Object.values(state.discoveredCards).reduce( + (sum, types) => sum + types.length, + 0, + ); - const bonus = 1 + inactiveCards * GAME_CONFIG.INCOME.INACTIVE_CARD_BONUS; + const bonus = 1 + totalDiscoveries * GAME_CONFIG.INCOME.INACTIVE_CARD_BONUS; const upgradeBonus = 1 + state.upgrades.seeds * GAME_CONFIG.UPGRADES.seeds.BONUS_PER_LEVEL; diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts index 1e899c9..0675bd5 100644 --- a/src/store/gameStore.ts +++ b/src/store/gameStore.ts @@ -1,12 +1,16 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { generateCard } from "./cardUtils"; +import { GAME_CONFIG } from "@/config/gameConfig"; import { createCurrencySlice, CurrencySlice } from "./slices/currencySlice"; import { createInventorySlice, InventorySlice } from "./slices/inventorySlice"; import { createDimensionSlice, DimensionSlice } from "./slices/dimensionSlice"; import { createUpgradeSlice, UpgradeSlice } from "./slices/upgradeSlice"; import { createPackSlice, PackSlice } from "./slices/packSlice"; +import { createCollectionSlice, CollectionSlice } from "./slices/collectionSlice"; +import { createAutoOpenSlice, AutoOpenSlice } from "./slices/autoOpenSlice"; +import { createCraftingSlice, CraftingSlice } from "./slices/craftingSlice"; import { migrateGameStore } from "./migrations"; // Re-export utilities for component usage @@ -16,7 +20,10 @@ export type GameStore = CurrencySlice & InventorySlice & DimensionSlice & UpgradeSlice & - PackSlice; + PackSlice & + CollectionSlice & + AutoOpenSlice & + CraftingSlice; export const useGameStore = create()( persist( @@ -26,18 +33,31 @@ export const useGameStore = create()( ...createDimensionSlice(...a), ...createUpgradeSlice(...a), ...createPackSlice(...a), + ...createCollectionSlice(...a), + ...createAutoOpenSlice(...a), + ...createCraftingSlice(...a), }), { name: "rick-morty-idle-save", - version: 6, + version: 8, migrate: migrateGameStore, - onRehydrateStorage: () => (state: GameStore | undefined) => { - if (state && state.inventory.length === 0) { - const starter = generateCard( - { COMMON: 0.8, RARE: 0.2, HOLO: 0, FULL_ART: 0 }, - 0, - ); - state.addCard(starter); + onRehydrateStorage: () => (state) => { + if (state) { + // Recalculate maxInventory to ensure consistency + // Check if upgrades and dimensionInventoryBonus exist before accessing + const upgradeBonus = (state.upgrades?.inventory || 0) * GAME_CONFIG.UPGRADES.inventory.BONUS_PER_LEVEL; + const dimensionBonus = state.dimensionInventoryBonus || 0; + + state.maxInventory = GAME_CONFIG.INITIAL_MAX_INVENTORY + upgradeBonus + dimensionBonus; + + if (state.inventory && state.inventory.length === 0) { + const starter = generateCard( + { COMMON: 0.8, RARE: 0.2, HOLO: 0, FULL_ART: 0 }, + 0, + ); + state.addCard(starter); + state.addDiscovery(starter.characterId, starter.types); + } } }, }, diff --git a/src/store/migrations.ts b/src/store/migrations.ts index adb238e..36c1dce 100644 --- a/src/store/migrations.ts +++ b/src/store/migrations.ts @@ -116,5 +116,22 @@ export const migrateGameStore = (persistedState: unknown, version: number) => { }; } + if (version < 8) { + const discovered: Record = { ...state.discoveredCards }; + const inventory = state.inventory || []; + + inventory.forEach((card: any) => { + if (!card || !card.characterId) return; + const types = card.types || []; + const currentTypes = discovered[card.characterId] || []; + discovered[card.characterId] = [...new Set([...currentTypes, ...types])]; + }); + + state = { + ...state, + discoveredCards: discovered, + }; + } + return state; }; diff --git a/src/store/slices/autoOpenSlice.ts b/src/store/slices/autoOpenSlice.ts new file mode 100644 index 0000000..6f9df75 --- /dev/null +++ b/src/store/slices/autoOpenSlice.ts @@ -0,0 +1,44 @@ +import { StateCreator } from "zustand"; +import { GameStore } from "../gameStore"; +import { GameCard } from "@/types/game"; + +export interface AutoOpenSlice { + isAutoOpenActive: boolean; + activePackId: string | null; + autoOpenHistory: GameCard[]; + startAutoOpen: (packId: string) => void; + stopAutoOpen: () => void; + addToAutoOpenHistory: (cards: GameCard[]) => void; +} + +export const createAutoOpenSlice: StateCreator< + GameStore, + [], + [], + AutoOpenSlice +> = (set) => ({ + isAutoOpenActive: false, + activePackId: null, + autoOpenHistory: [], + + startAutoOpen: (packId) => { + set({ + isAutoOpenActive: true, + activePackId: packId, + autoOpenHistory: [], // Reset history when starting a new one + }); + }, + + stopAutoOpen: () => { + set({ + isAutoOpenActive: false, + activePackId: null, + }); + }, + + addToAutoOpenHistory: (cards) => { + set((state) => ({ + autoOpenHistory: [...cards, ...state.autoOpenHistory].slice(0, 10), + })); + }, +}); diff --git a/src/store/slices/collectionSlice.ts b/src/store/slices/collectionSlice.ts new file mode 100644 index 0000000..b4cf392 --- /dev/null +++ b/src/store/slices/collectionSlice.ts @@ -0,0 +1,36 @@ +import { StateCreator } from "zustand"; +import { GameStore } from "../gameStore"; + +export interface CollectionSlice { + discoveredCards: Record; // characterId -> array of rarity typeIds + addDiscovery: (characterId: number, types: string[]) => void; + resetCollection: () => void; +} + +export const createCollectionSlice: StateCreator< + GameStore, + [], + [], + CollectionSlice +> = (set) => ({ + discoveredCards: {}, + + addDiscovery: (characterId, types) => { + set((state) => { + const currentTypes = state.discoveredCards[characterId] || []; + const newTypes = [...new Set([...currentTypes, ...types])]; + + // Only update if there are actually new types discovered + if (newTypes.length === currentTypes.length) return state; + + return { + discoveredCards: { + ...state.discoveredCards, + [characterId]: newTypes, + }, + }; + }); + }, + + resetCollection: () => set({ discoveredCards: {} }), +}); diff --git a/src/store/slices/craftingSlice.ts b/src/store/slices/craftingSlice.ts new file mode 100644 index 0000000..c5f5081 --- /dev/null +++ b/src/store/slices/craftingSlice.ts @@ -0,0 +1,82 @@ +import { StateCreator } from "zustand"; +import { GameStore } from "../gameStore"; +import { GameCard } from "@/types/game"; +import { getPrimaryType } from "@/config/rarityConfig"; +import { generateCard } from "../cardUtils"; +import { toast } from "sonner"; + +export interface CraftingSlice { + spliceCards: (cardIds: string[]) => GameCard | null; +} + +const RARITY_ORDER = [ + "COMMON", + "RARE", + "HOLO", + "FULL_ART", + "SILVER", + "GOLD", + "REVERT", +]; + +export const createCraftingSlice: StateCreator< + GameStore, + [], + [], + CraftingSlice +> = (set, get) => ({ + spliceCards: (cardIds) => { + const { inventory, addCard, addDiscovery } = get(); + + if (cardIds.length !== 5) { + toast.error("Splicer requires exactly 5 cards!"); + return null; + } + + const cardsToSplice = inventory.filter((c) => cardIds.includes(c.id)); + if (cardsToSplice.length !== 5) return null; + + // Determine primary rarity of the batch (must be all the same for guaranteed upgrade) + const rarities = cardsToSplice.map((c) => getPrimaryType(c.types)); + const firstRarity = rarities[0]; + const allSame = rarities.every((r) => r === firstRarity); + + if (!allSame) { + toast.error("Mixed signals!", { + description: "All cards must be of the same rarity tier.", + }); + return null; + } + + const currentIndex = RARITY_ORDER.indexOf(firstRarity); + if (currentIndex === -1 || currentIndex === RARITY_ORDER.length - 1) { + toast.error("Maximum resonance reached!", { + description: "These cards cannot be upgraded further.", + }); + return null; + } + + const nextRarity = RARITY_ORDER[currentIndex + 1]; + + // Remove old cards + set((state) => ({ + inventory: state.inventory.filter((c) => !cardIds.includes(c.id)), + activeSlots: state.activeSlots.map((slot) => + slot && cardIds.includes(slot.id) ? null : slot, + ), + })); + + // Generate new card of next rarity + // We pass a weights object with 100% chance for the next rarity + const newCard = generateCard( + { [nextRarity]: 1 }, + 0.1, + cardsToSplice[0]?.characterId, + ); // Small chance for additional random type + + addCard(newCard); + addDiscovery(newCard.characterId, newCard.types); + + return newCard; + }, +}); diff --git a/src/store/slices/currencySlice.ts b/src/store/slices/currencySlice.ts index ceda443..2173dd1 100644 --- a/src/store/slices/currencySlice.ts +++ b/src/store/slices/currencySlice.ts @@ -35,6 +35,7 @@ export const createCurrencySlice: StateCreator< maxDimensionLevel: 1, isDimensionActive: false, currentEnemy: null, + dimensionInventoryBonus: 0, upgrades: { seeds: 0, power: 0, inventory: 0 }, lastSaved: Date.now(), }); diff --git a/src/store/slices/dimensionSlice.ts b/src/store/slices/dimensionSlice.ts index 1f3557c..0707ac9 100644 --- a/src/store/slices/dimensionSlice.ts +++ b/src/store/slices/dimensionSlice.ts @@ -10,8 +10,9 @@ export interface DimensionSlice { maxDimensionLevel: number; isDimensionActive: boolean; currentEnemy: GameCard | null; + dimensionInventoryBonus: number; // New field for permanent bonus startDimension: () => boolean; - nextDimensionLevel: () => { bonus: number; milestoneUnlocked: string | null }; + nextDimensionLevel: () => { bonus: number; inventoryBonus: number; milestoneUnlocked: string | null }; resetDimension: (reward: number) => void; } @@ -25,6 +26,7 @@ export const createDimensionSlice: StateCreator< maxDimensionLevel: 1, isDimensionActive: false, currentEnemy: null, + dimensionInventoryBonus: 0, startDimension: () => { const { seeds } = get(); @@ -49,15 +51,22 @@ export const createDimensionSlice: StateCreator< }, nextDimensionLevel: () => { - const { dimensionLevel } = get(); + const { dimensionLevel, maxDimensionLevel } = get(); let bonus = 0; + let inventoryBonus = 0; let milestoneUnlocked = null; - if ((dimensionLevel + 1) % GAME_CONFIG.DIMENSIONS.BONUS_STEP === 0) { - bonus = (dimensionLevel + 1) * GAME_CONFIG.DIMENSIONS.BONUS_AMOUNT; + const nextLvl = dimensionLevel + 1; + const isNewMax = nextLvl > maxDimensionLevel; + + if (nextLvl % GAME_CONFIG.DIMENSIONS.BONUS_STEP === 0) { + bonus = nextLvl * GAME_CONFIG.DIMENSIONS.BONUS_AMOUNT; + // Only grant permanent inventory bonus if this is the first time reaching this milestone + if (isNewMax) { + inventoryBonus = GAME_CONFIG.DIMENSIONS.INVENTORY_BONUS_PER_STEP; + } } - const nextLvl = dimensionLevel + 1; milestoneUnlocked = GAME_CONFIG.DIMENSIONS.MILESTONES[nextLvl] || null; const newEnemy = generateCard( @@ -71,17 +80,23 @@ export const createDimensionSlice: StateCreator< newEnemy.income = stats.income; set((s) => { - const nextLevel = s.dimensionLevel + 1; - const newMax = Math.max(s.maxDimensionLevel, nextLevel); + const newMax = Math.max(s.maxDimensionLevel, nextLvl); + + const newDimensionBonus = s.dimensionInventoryBonus + inventoryBonus; + const upgradeBonus = s.upgrades.inventory * GAME_CONFIG.UPGRADES.inventory.BONUS_PER_LEVEL; + const newMaxInventory = GAME_CONFIG.INITIAL_MAX_INVENTORY + upgradeBonus + newDimensionBonus; + return { seeds: s.seeds + bonus, - dimensionLevel: nextLevel, + dimensionInventoryBonus: newDimensionBonus, + maxInventory: newMaxInventory, + dimensionLevel: nextLvl, maxDimensionLevel: newMax, currentEnemy: newEnemy, }; }); - return { bonus, milestoneUnlocked }; + return { bonus, inventoryBonus, milestoneUnlocked }; }, resetDimension: (reward) => { diff --git a/src/store/slices/inventorySlice.ts b/src/store/slices/inventorySlice.ts index 30ad39f..78e997e 100644 --- a/src/store/slices/inventorySlice.ts +++ b/src/store/slices/inventorySlice.ts @@ -26,19 +26,21 @@ export const createInventorySlice: StateCreator< activeSlots: [null, null, null, null], addCard: (card) => { - const { inventory, maxInventory } = get(); + const { inventory, maxInventory, addDiscovery } = get(); if (inventory.length >= maxInventory) return false; set((s) => ({ inventory: [...s.inventory, card] })); + addDiscovery(card.characterId, card.types); return true; }, addCards: (cards) => { - const { inventory, maxInventory } = get(); + const { inventory, maxInventory, addDiscovery } = get(); const availableSpace = maxInventory - inventory.length; if (availableSpace <= 0) return false; const cardsToAdd = cards.slice(0, availableSpace); set((s) => ({ inventory: [...s.inventory, ...cardsToAdd] })); + cardsToAdd.forEach((card) => addDiscovery(card.characterId, card.types)); return true; }, diff --git a/src/store/slices/upgradeSlice.ts b/src/store/slices/upgradeSlice.ts index 2a90093..1b17346 100644 --- a/src/store/slices/upgradeSlice.ts +++ b/src/store/slices/upgradeSlice.ts @@ -58,9 +58,11 @@ export const createUpgradeSlice: StateCreator< }; if (type === "inventory") { + const currentDimensionBonus = s.dimensionInventoryBonus || 0; newState.maxInventory = GAME_CONFIG.INITIAL_MAX_INVENTORY + - newLevel * GAME_CONFIG.UPGRADES.inventory.BONUS_PER_LEVEL; + newLevel * GAME_CONFIG.UPGRADES.inventory.BONUS_PER_LEVEL + + currentDimensionBonus; } return newState; diff --git a/src/types/game.ts b/src/types/game.ts index 2df03e6..5847cea 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -16,7 +16,7 @@ export interface Character { origin: string; location: string; avatarId: number; - baseMultiplier: number; + iq: number; basePower: number; customImage?: string; } @@ -44,5 +44,6 @@ export interface GameState { power: number; inventory: number; }; + discoveredCards: Record; // characterId -> array of rarity typeIds lastSaved: number; }