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}
+ ))}
+
+
+
+
+ );
+ })}
+
+
+
+
+ Inventory:
+ = maxInventory ? "text-red-500" : "text-primary"}>
+ {inventory.length}/{maxInventory}
+
+
+
+ STOP
+
+
+
+
+
+ );
+}
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 ? "LOCKED" : `Buy for ${formatCurrency(pack.cost)}`}
-
+
+
+ {!isUnlocked ? "LOCKED" : `Buy for ${formatCurrency(pack.cost)}`}
+
+ {isUnlocked && (
+
+ {isThisPackAutoOpening ? (
+ <> STOP AUTO>
+ ) : (
+ <> AUTO OPEN>
+ )}
+
+ )}
+
{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 = () => {
-
+
@@ -108,122 +146,264 @@ const Collection = () => {
-
Your Collection
-
{inventory.length} / {maxInventory} cards collected
+
Citadel Archives
+
+
Inventory: {inventory.length} / {maxInventory}
+
Registry: {pokedexStats.discoveredCount} / {pokedexStats.totalCharacters}
+
-
- {
- setIsSellMode(!isSellMode);
- setSelectedIds([]);
- }}
- className={`font-bold transition-colors ${!isSellMode ? 'hover:bg-destructive hover:text-destructive-foreground hover:border-destructive' : ''}`}
- >
- {isSellMode ? "Cancel Sell Mode" : "Sell Mode"}
-
-
+
+
+
+
+
+ Inventory
+
+
+
+ Archives
+
+
+
+ {activeTab === 'archives' && (
+
+
+
+
+
+
+ Dimensional Discovery
+
+ {pokedexStats.percentage}%
+
+
+
+
+
+
+
+
+ Rarity Resonance
+
+ {pokedexStats.rarityPercentage}%
+
+
+
+
+
+ )}
+
{/* Filters */}
setSearch(e.target.value)}
/>
-
-
-
-
-
- All Types
- Common
- Rare
- Holo
- Full Art
- Silver
- Gold
- Revert
-
-
+ {activeTab === 'inventory' ? (
+ <>
+
+
+
+
+
+ All Types
+ {cardTypesData.map(t => (
+ {t.label}
+ ))}
+
+
-
-
-
-
-
- Newest First
- Oldest First
- Highest Income
-
-
+
+
+
+
+
+ Newest First
+ Oldest First
+ Highest Income
+ Highest IQ
+ Highest Power
+
+
+ >
+ ) : (
+
+
+ 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 ? (
-
- ) : (
-
+
+
+
+ {
+ setIsSellMode(!isSellMode);
+ setSelectedIds([]);
+ }}
+ className={`font-bold transition-colors ${!isSellMode ? 'hover:bg-destructive hover:text-destructive-foreground hover:border-destructive' : ''}`}
+ >
+ {isSellMode ? "Cancel Sell Mode" : "Sell Mode"}
+
+
+
+ {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.
+
+
{ setSearch(''); setTypeFilter('ALL'); }}>
+ Clear All Filters
+
+
+ )}
+
+
+
+
+ {filteredPokedex.map((char) => {
+ const typesDiscovered = discoveredCards[char.id] || [];
+ const isDiscovered = typesDiscovered.length > 0;
+
+ return (
+
+
+
+ {!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')}
+
+
+
+
+ );
+ })}
- { setSearch(''); setTypeFilter('ALL'); }}>
- Clear All Filters
-
-
- )}
+
+
{/* Sell Mode Floating Bar */}
- {isSellMode && (
-
-
-
+ {isSellMode && activeTab === 'inventory' && (
+
+
+
{selectedIds.length} Selected
Total: {Math.floor(selectedTotalProfit).toLocaleString()} Seeds
-
+
{
+ const allFilteredIds = filteredCards.map(f => f.card.id);
+ const allSelected = allFilteredIds.every(id => selectedIds.includes(id));
+
+ if (allSelected) {
+ setSelectedIds(prev => prev.filter(id => !allFilteredIds.includes(id)));
+ } else {
+ setSelectedIds(prev => Array.from(new Set([...prev, ...allFilteredIds])));
+ }
+ }}
+ >
+ {filteredCards.map(f => f.card.id).every(id => selectedIds.includes(id))
+ ? "Deselect All"
+ : "Select All Visible"}
+
+
setSelectedIds([])}
>
Clear
@@ -231,7 +411,7 @@ const Collection = () => {
setIsConfirmOpen(true)}
disabled={selectedIds.length === 0}
>
diff --git a/src/pages/Dimension.tsx b/src/pages/Dimension.tsx
index dd3d9d8..e476297 100644
--- a/src/pages/Dimension.tsx
+++ b/src/pages/Dimension.tsx
@@ -3,13 +3,28 @@ import { Header } from "@/components/game/Header";
import { Footer } from "@/components/game/Footer";
import { Button } from "@/components/ui/button";
import { Link } from "react-router-dom";
-import { Map, ArrowLeft, Sword, History, Zap, Beaker } from "lucide-react";
+import {
+ Map,
+ ArrowLeft,
+ Sword,
+ History,
+ Zap,
+ Beaker,
+ ChevronDown,
+ ChevronUp,
+ Trophy,
+} from "lucide-react";
import { useGameStore, resolveCardStats } from "@/store/gameStore";
import { GameCard as GameCardType } from "@/types/game";
import { GameCard } from "@/components/game/GameCard";
import { toast } from "sonner";
import { formatNumber, formatCurrency } from "@/lib/utils";
import { GAME_CONFIG } from "@/config/gameConfig";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
const Dimension = () => {
const {
@@ -26,31 +41,47 @@ const Dimension = () => {
} = useGameStore();
const [isFighting, setIsFighting] = useState(false);
+ const [showMilestones, setShowMilestones] = useState(false);
+
+ const [battleState, setBattleState] = useState<{
+ playerAttacking: boolean;
+ enemyAttacking: boolean;
+ playerDamage: number | null;
+ enemyDamage: number | null;
+ }>({
+ playerAttacking: false,
+ enemyAttacking: false,
+ playerDamage: null,
+ enemyDamage: null,
+ });
// Find player's strongest card and calculate base vs bonus
const playerStats = useMemo(() => {
if (inventory.length === 0) return null;
-
+
// Process inventory to resolve stats first
const resolvedInventory = inventory
.filter(Boolean)
- .map(card => ({
+ .map((card) => ({
card,
- stats: resolveCardStats(card)
+ stats: resolveCardStats(card),
}))
.sort((a, b) => b.stats.power - a.stats.power);
-
+
const strongest = resolvedInventory[0];
// Lab Power Upgrade
- const powerMultiplier = 1 + upgrades.power * GAME_CONFIG.UPGRADES.power.BONUS_PER_LEVEL;
+ const powerMultiplier =
+ 1 + upgrades.power * GAME_CONFIG.UPGRADES.power.BONUS_PER_LEVEL;
const totalPower = Math.floor(strongest.stats.power * powerMultiplier);
return {
card: strongest.card,
basePower: strongest.stats.power,
totalPower,
- bonusPercent: Math.round(upgrades.power * GAME_CONFIG.UPGRADES.power.BONUS_PER_LEVEL * 100),
+ bonusPercent: Math.round(
+ upgrades.power * GAME_CONFIG.UPGRADES.power.BONUS_PER_LEVEL * 100,
+ ),
};
}, [inventory, upgrades.power]);
@@ -66,43 +97,76 @@ const Dimension = () => {
}
};
- const handleFight = () => {
+ const handleFight = async () => {
if (!playerStats || !currentEnemy) return;
-
- const enemyStats = resolveCardStats(currentEnemy);
+ const enemyStats = resolveCardStats(currentEnemy);
setIsFighting(true);
+ const playerWins = playerStats.totalPower >= enemyStats.power;
+
+ // 1. Player Attack
+ setBattleState((s) => ({ ...s, playerAttacking: true }));
+
+ await new Promise((r) => setTimeout(r, 300));
+ // Damage pops up on enemy
+ setBattleState((s) => ({ ...s, enemyDamage: playerStats.totalPower }));
+
+ await new Promise((r) => setTimeout(r, 400));
+ setBattleState((s) => ({ ...s, playerAttacking: false }));
+ // 2. Enemy Attack - ONLY if player didn't win yet
+ if (!playerWins) {
+ await new Promise((r) => setTimeout(r, 200));
+ setBattleState((s) => ({ ...s, enemyAttacking: true }));
+
+ await new Promise((r) => setTimeout(r, 300));
+ // Damage pops up on player
+ setBattleState((s) => ({ ...s, playerDamage: enemyStats.power }));
+
+ await new Promise((r) => setTimeout(r, 400));
+ setBattleState((s) => ({ ...s, enemyAttacking: false }));
+ }
+
+ // 3. Final Result
setTimeout(() => {
if (dimensionLevel >= GAME_CONFIG.DIMENSIONS.MAX_LEVEL) {
toast.success(`EPIC VICTORY! You conquered the Dimension Rift!`, {
description: `You've reached the maximum level and unlocked all rewards!`,
});
resetDimension(GAME_CONFIG.DIMENSIONS.MAX_LEVEL_REWARD);
- } else if (playerStats.totalPower >= enemyStats.power) {
- const { bonus, milestoneUnlocked } = nextDimensionLevel();
+ } else if (playerWins) {
+ const { bonus, inventoryBonus, milestoneUnlocked } =
+ nextDimensionLevel();
if (milestoneUnlocked) {
toast.success(`MILESTONE REACHED!`, {
- description: `You unlocked: ${milestoneUnlocked}!`,
+ description: `You unlocked: ${milestoneUnlocked}!${inventoryBonus > 0 ? ` +${inventoryBonus} Card Slots!` : ""}`,
duration: 5000,
});
} else if (bonus > 0) {
toast.success(`Victory! Level ${dimensionLevel} cleared.`, {
- description: `Milestone Bonus: +${formatCurrency(bonus)} Mega Seeds!`,
+ description: `Bonus: +${formatCurrency(bonus)} Mega Seeds${inventoryBonus > 0 ? ` & +${inventoryBonus} Card Slots!` : ""}`,
});
} else {
toast.success(`Victory! Level ${dimensionLevel} cleared.`);
}
} else {
- const reward = dimensionLevel * GAME_CONFIG.DIMENSIONS.LEVEL_REWARD_MULTIPLIER;
+ const reward =
+ dimensionLevel * GAME_CONFIG.DIMENSIONS.LEVEL_REWARD_MULTIPLIER;
toast.error(`Defeat! You reached level ${dimensionLevel}.`, {
description: `Final Reward: ${formatCurrency(reward)} Mega Seeds.`,
});
resetDimension(reward);
}
+
setIsFighting(false);
- }, 1500);
+ setBattleState({
+ playerAttacking: false,
+ enemyAttacking: false,
+ playerDamage: null,
+ enemyDamage: null,
+ });
+ }, 400);
};
const enemyPower = useMemo(() => {
@@ -111,7 +175,7 @@ const Dimension = () => {
}, [currentEnemy]);
return (
-
+
@@ -137,7 +201,13 @@ const Dimension = () => {
- Lab Bonus: +{Math.round(upgrades.power * GAME_CONFIG.UPGRADES.power.BONUS_PER_LEVEL * 100)}% Power
+ Lab Bonus: +
+ {Math.round(
+ upgrades.power *
+ GAME_CONFIG.UPGRADES.power.BONUS_PER_LEVEL *
+ 100,
+ )}
+ % Power
@@ -148,6 +218,159 @@ const Dimension = () => {
+ {/* Milestone Progress Section */}
+
+
+
+
+
+
+
+
+
+ Citadel Milestones
+
+
+ Current Rank:{" "}
+ {maxDimensionLevel >= 100
+ ? "Dimension Master"
+ : maxDimensionLevel >= 50
+ ? "Alchemist"
+ : maxDimensionLevel >= 25
+ ? "Rift Walker"
+ : maxDimensionLevel >= 10
+ ? "Novice Traveler"
+ : "Lost Soul"}
+
+
+
+
+
+
+
+ {Math.min(
+ 100,
+ Math.round((maxDimensionLevel / 100) * 100),
+ )}
+ %
+
+
+ Progress
+
+
+ {showMilestones ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ 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 = () => {
-
+
Portal Shop
+
+
+
+ Genetic Splicer
+
+
-
+
Dimension Rift
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 => (
+ handleFilterChange(type.id)}
+ className={`rounded-full text-[10px] h-7 px-3 border-${type.id.toLowerCase()}/20`}
+ >
+ {type.label}
+
+ ))}
+
+
+
+
+ Ready
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+ {selectedIds.length > 0 && (
+
setSelectedIds([])}
+ >
+
+
+ )}
+
+
+
+ {/* 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 ? (
+
setResultCard(null)}
+ variant="outline"
+ className="w-full py-6 font-display font-bold hover:bg-primary/10 hover:text-primary transition-all group"
+ >
+
+ EXTRACT SPECIMEN
+
+ ) : (
+
+ {isSplicing ? (
+
+ ) : isValidBatch ? (
+ "INITIATE FUSION"
+ ) : (
+ `NEED ${5 - selectedIds.length} MORE`
+ )}
+
+ )}
+
+
+
+
+ {/* 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;
}