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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ 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
}
```

- **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.
Expand Down
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();

Expand Down Expand Up @@ -80,6 +82,7 @@ const App = () => {
<TooltipProvider>
<Toaster />
<Sonner />
<AutoOpenManager />
<BrowserRouter>
<AnalyticsTracker />
<Routes>
Expand All @@ -89,6 +92,7 @@ const App = () => {
<Route path="/settings" element={<Settings />} />
<Route path="/dimension" element={<Dimension />} />
<Route path="/upgrades" element={<Upgrades />} />
<Route path="/splicer" element={<Splicer />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
Expand Down
152 changes: 152 additions & 0 deletions src/components/game/AutoOpenManager.tsx
Original file line number Diff line number Diff line change
@@ -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<NodeJS.Timeout | null>(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 (
<div className="fixed bottom-24 right-6 z-[60] w-72 animate-in slide-in-from-right-10 duration-500">
<div className="bg-card border-2 border-primary/40 rounded-2xl shadow-2xl overflow-hidden backdrop-blur-md">
<div className="bg-primary/20 p-3 flex items-center justify-between border-b border-primary/30">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 text-primary animate-spin" />
<span className="font-display font-bold text-xs tracking-wider uppercase">Auto-Opening</span>
</div>
<Button variant="ghost" size="icon" className="h-6 w-6 rounded-full hover:bg-destructive/20" onClick={stopAutoOpen}>
<X className="w-4 h-4" />
</Button>
</div>

<div className="p-4 space-y-4">
<div className="flex justify-between items-center text-[10px] uppercase font-bold text-muted-foreground">
<span>{activePack.name}</span>
<span>{formatCurrency(activePack.cost)}</span>
</div>

<div className="space-y-2 max-h-48 overflow-y-auto pr-2 custom-scrollbar">
{autoOpenHistory.length === 0 && (
<p className="text-center text-xs text-muted-foreground py-4 italic">Stabilizing portal...</p>
)}
{autoOpenHistory.map((card, i) => {
const stats = resolveCardStats(card);
return (
<div key={card.id + i} className="flex items-center gap-3 p-2 bg-muted/40 rounded-lg border border-border/50 animate-in slide-in-from-top-2 duration-300">
<img
src={stats.character.customImage || `https://rickandmortyapi.com/api/character/avatar/${stats.character.avatarId}.jpeg`}
className="w-8 h-8 rounded-md object-cover border border-primary/30"
alt=""
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<p className="text-[10px] font-bold truncate">{stats.character.name}</p>
<span className="text-[7px] font-bold text-purple-400">IQ {stats.character.iq}</span>
</div>
<div className="flex gap-1">
{card.types.map(t => (
<span key={t} className="text-[6px] px-1 rounded-sm bg-primary/20 text-primary-foreground font-bold">{t}</span>
))}
</div>
</div>
<div className="text-right">
<p className="text-[8px] font-bold text-primary">+{stats.income}</p>
</div>
</div>
);
})}
</div>

<div className="pt-2 border-t border-border flex justify-between items-center">
<div className="text-[10px]">
<span className="text-muted-foreground uppercase mr-1">Inventory:</span>
<span className={inventory.length >= maxInventory ? "text-red-500" : "text-primary"}>
{inventory.length}/{maxInventory}
</span>
</div>
<Button size="sm" variant="destructive" className="h-7 text-[10px] font-bold" onClick={stopAutoOpen}>
STOP
</Button>
</div>
</div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/game/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
</p>
<div className="flex items-center justify-between">
<CardTypes types={types} />
<CardStats income={income} power={power} />
<CardStats income={income} power={power} iq={character.iq} />
</div>
<div className="pt-1 border-t border-border/50 flex items-center justify-between opacity-60">
<span className="text-[8px] font-body uppercase">
Expand Down
12 changes: 7 additions & 5 deletions src/components/game/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<header className="flex items-center justify-between px-6 py-4 border-b border-border bg-card/60 backdrop-blur-md">
Expand Down
65 changes: 54 additions & 11 deletions src/components/game/PackOpening.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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(
Expand All @@ -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];
Expand Down Expand Up @@ -211,14 +238,30 @@ export function PackOpening({ packId }: PackOpeningProps) {
</div>
</div>

<Button
onClick={handleBuyPack}
disabled={seeds < pack.cost || !isUnlocked}
className="font-display font-bold min-w-[140px] shadow-xl"
variant={!isUnlocked ? "outline" : "default"}
>
{!isUnlocked ? "LOCKED" : `Buy for ${formatCurrency(pack.cost)}`}
</Button>
<div className="flex flex-col gap-2">
<Button
onClick={handleBuyPack}
disabled={seeds < pack.cost || !isUnlocked}
className="font-display font-bold min-w-[140px] shadow-xl"
variant={!isUnlocked ? "outline" : "default"}
>
{!isUnlocked ? "LOCKED" : `Buy for ${formatCurrency(pack.cost)}`}
</Button>
{isUnlocked && (
<Button
onClick={handleToggleAutoOpen}
disabled={seeds < pack.cost}
variant={isThisPackAutoOpening ? "destructive" : "secondary"}
className="font-display font-bold text-xs h-8"
>
{isThisPackAutoOpening ? (
<><Pause className="w-3 h-3 mr-2" /> STOP AUTO</>
) : (
<><Play className="w-3 h-3 mr-2" /> AUTO OPEN</>
)}
</Button>
)}
</div>

{isOpen && (
<div className="fixed inset-0 z-[100] bg-background/95 backdrop-blur-2xl overflow-y-auto">
Expand Down Expand Up @@ -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"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(var(--primary),0.1),transparent)] group-hover:opacity-100 opacity-0 transition-opacity" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.1),transparent)] group-hover:opacity-100 opacity-0 transition-opacity" />
<div className="w-14 h-14 rounded-full bg-background/50 flex items-center justify-center group-hover:rotate-12 transition-transform shadow-inner">
<ChevronRight className="w-6 h-6 text-muted-foreground group-hover:text-primary" />
</div>
Expand Down
8 changes: 2 additions & 6 deletions src/components/game/PortalArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<section className="py-8 px-4">
Expand All @@ -17,11 +17,7 @@ export function PortalArea() {
Portal Slots
</h2>
<p className="text-sm text-muted-foreground font-body">
Place cards to generate Mega Seeds • Collection bonus:{" "}
<span className="text-primary font-bold">
+{Math.max(0, Math.floor((inventory.length - activeCount) / 2))}%
</span>{" "}
({inactiveCards} cards in inventory)
Place cards to generate Mega Seeds
</p>
</div>

Expand Down
11 changes: 9 additions & 2 deletions src/components/game/card-parts/CardStats.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-end">
<div className="flex items-center gap-1">
Expand All @@ -21,6 +22,12 @@ export function CardStats({ income, power }: CardStatsProps) {
{formatNumber(power)}
</span>
</div>
{iq !== undefined && (
<div className="flex items-center gap-1 mb-0.5">
<Brain className="w-2 h-2 text-purple-400 fill-purple-400/20" />
<span className="text-[9px] font-bold text-purple-400">{iq}</span>
</div>
)}
</div>
);
}
Loading
Loading