diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index d72682a..343ef85 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -32,7 +32,8 @@ jobs:
- name: Install dependencies
run: npm install
-
+ - name: Prebuild
+ run: npm run prebuild
- name: Build
env:
VITE_GA_ID: ${{ secrets.VITE_GA_ID }}
diff --git a/src/App.tsx b/src/App.tsx
index ad44f5c..d79452a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,10 +12,10 @@ import Upgrades from "./pages/Upgrades";
import NotFound from "./pages/NotFound";
import { useEffect, useRef } from "react";
-import { useGameStore } from "@/store/gameStore";
+import { useGameStore, calculateCurrentIncome } from "@/store/gameStore";
import { toast } from "sonner";
import { formatCurrency } from "@/lib/utils";
-import { GAME_CONFIG, calculateCurrentIncome } from "@/config/gameConfig";
+import { GAME_CONFIG } from "@/config/gameConfig";
import { initGA, trackPageView } from "@/lib/analytics";
import { useLocation } from "react-router-dom";
diff --git a/src/components/game/CollectionTab.tsx b/src/components/game/CollectionTab.tsx
index 6ed34de..44becf3 100644
--- a/src/components/game/CollectionTab.tsx
+++ b/src/components/game/CollectionTab.tsx
@@ -1,4 +1,4 @@
-import { useGameStore } from '@/store/gameStore';
+import { useGameStore, resolveCardStats } from '@/store/gameStore';
import { GameCard } from './GameCard';
import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
@@ -26,11 +26,12 @@ export function CollectionTab() {
const displayCards = useMemo(() => {
return inventory
- .filter(card =>
- card.name.toLowerCase().includes(search.toLowerCase()) ||
- card.characterName.toLowerCase().includes(search.toLowerCase())
+ .filter(Boolean)
+ .map((card) => ({ card, stats: resolveCardStats(card) }))
+ .filter(({ stats }) =>
+ stats.character.name.toLowerCase().includes(search.toLowerCase()),
)
- .sort((a, b) => b.income - a.income)
+ .sort((a, b) => b.stats.income - a.stats.income)
.slice(0, 4);
}, [inventory, search]);
@@ -62,7 +63,7 @@ export function CollectionTab() {
{displayCards.length > 0 ? (
- {displayCards.map((card) => {
+ {displayCards.map(({ card }) => {
const isInSlot = activeSlots.some((s) => s?.id === card.id);
return (
diff --git a/src/components/game/GameCard.tsx b/src/components/game/GameCard.tsx
index 659b12f..1d3adc7 100644
--- a/src/components/game/GameCard.tsx
+++ b/src/components/game/GameCard.tsx
@@ -8,9 +8,11 @@ import {
Trophy,
RefreshCw,
Sword,
+ Leaf,
+ LucideIcon,
} from "lucide-react";
import { formatNumber } from "@/lib/utils";
-import cardTypes from "@/data/cardTypes.json";
+import { resolveCardStats } from "@/store/gameStore";
interface GameCardProps {
card: GameCardType;
@@ -18,7 +20,18 @@ interface GameCardProps {
isActive?: boolean;
}
-const typeIcons: Record
= {
+type RarityOrType =
+ | "COMMON"
+ | "NORMAL"
+ | "RARE"
+ | "HOLO"
+ | "FULL_ART"
+ | "SILVER"
+ | "GOLD"
+ | "REVERT"
+ | "SWORD";
+
+const typeIcons = {
COMMON: Circle,
NORMAL: Circle,
RARE: Star,
@@ -28,9 +41,9 @@ const typeIcons: Record = {
GOLD: Trophy,
REVERT: RefreshCw,
SWORD: Sword,
-};
+} satisfies Record;
-const rarityConfig: Record = {
+const rarityConfig = {
COMMON: { border: "border-border", bg: "bg-card" },
NORMAL: { border: "border-border", bg: "bg-card" },
RARE: { border: "border-blue-400/60 animate-rare-pulse", bg: "bg-card" },
@@ -39,15 +52,17 @@ const rarityConfig: Record = {
SILVER: { border: "border-slate-300", bg: "bg-slate-900/40" },
GOLD: { border: "border-yellow-500", bg: "bg-yellow-900/20" },
REVERT: { border: "border-red-500", bg: "bg-black" },
-};
+} satisfies Record;
export function GameCard({ card, onClick, isActive }: GameCardProps) {
+ const stats = resolveCardStats(card);
+ const { character, income, power } = stats;
+
const types = card.types || [];
const isHolo = types.includes("HOLO");
const isFullArt = types.includes("FULL_ART");
- const isRare = types.includes("RARE");
- const isSilver = types.includes("SILVER");
const isGold = types.includes("GOLD");
+ const isSilver = types.includes("SILVER");
const isRevert = types.includes("REVERT");
const primaryType = types.includes("REVERT")
@@ -68,12 +83,10 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
const config = rarityConfig[primaryType] || rarityConfig.COMMON;
- let imgSrc = "https://rickandmortyapi.com/api/character/avatar/19.jpeg";
+ let imgSrc = `https://rickandmortyapi.com/api/character/avatar/${character.avatarId}.jpeg`;
- if (card.customImage) {
- imgSrc = card.customImage;
- } else if (card.avatarId) {
- imgSrc = `https://rickandmortyapi.com/api/character/avatar/${card.avatarId}.jpeg`;
+ if (character.customImage) {
+ imgSrc = character.customImage;
}
const imageFilter = isRevert
@@ -106,7 +119,7 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
{isFullArt ? (
@@ -114,7 +127,7 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {

@@ -124,14 +137,14 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
- {card.status}
+ {character.status}
@@ -140,10 +153,10 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
className={`p-3 space-y-1 ${isFullArt ? "bg-background/80 backdrop-blur-sm" : ""}`}
>
- {card.characterName}
+ {character.name}
- {card.origin}
+ {character.origin}
@@ -156,19 +169,24 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
})}
-
- +{formatNumber(card.income)}/s
-
+
+
+
+ {formatNumber(income)}/s
+
+
- {formatNumber(card.power)}
+ {formatNumber(power)}
- {card.species}
+
+ {character.species}
+
diff --git a/src/components/game/Header.tsx b/src/components/game/Header.tsx
index ba9ab6f..ef41138 100644
--- a/src/components/game/Header.tsx
+++ b/src/components/game/Header.tsx
@@ -1,9 +1,9 @@
-import { useGameStore } from '@/store/gameStore';
+import { useGameStore, calculateCurrentIncome } from '@/store/gameStore';
import { Leaf, TrendingUp, Settings, Beaker, Library } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { formatNumber, formatCurrency } from '@/lib/utils';
-import { GAME_CONFIG, calculateCurrentIncome } from '@/config/gameConfig';
+import { GAME_CONFIG } from '@/config/gameConfig';
export function Header() {
const seeds = useGameStore((s) => s.seeds);
@@ -11,9 +11,8 @@ export function Header() {
const inventory = useGameStore((s) => s.inventory);
const upgrades = useGameStore((s) => s.upgrades);
- const inactiveCards = inventory.filter(
- (c) => !activeSlots.some((s) => s?.id === c.id)
- ).length;
+ const activeCount = activeSlots.filter(Boolean).length;
+ const inactiveCards = Math.max(0, inventory.length - activeCount);
const collectionBonus = Math.round(inactiveCards * GAME_CONFIG.INCOME.INACTIVE_CARD_BONUS * 100);
const labBonus = Math.round((upgrades.seeds || 0) * GAME_CONFIG.UPGRADES.seeds.BONUS_PER_LEVEL * 100);
diff --git a/src/components/game/PackOpening.tsx b/src/components/game/PackOpening.tsx
index 4a7cfe4..c66721b 100644
--- a/src/components/game/PackOpening.tsx
+++ b/src/components/game/PackOpening.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
-import { useGameStore } from "@/store/gameStore";
+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";
@@ -102,12 +102,13 @@ export function PackOpening({ packId }: PackOpeningProps) {
};
const handleSellCard = (card: GameCardType) => {
+ const stats = resolveCardStats(card);
sellCard(card.id, card);
setSoldCards((prev) => [...prev, card.id]);
const sellPrice = Math.floor(
- card.income * GAME_CONFIG.SELL_PRICE_MULTIPLIER,
+ stats.income * GAME_CONFIG.SELL_PRICE_MULTIPLIER,
);
- toast.success(`${card.characterName} sold!`, {
+ toast.success(`${stats.character.name} sold!`, {
description: `Gained ${formatCurrency(sellPrice)} Mega Seeds.`,
});
};
@@ -300,7 +301,7 @@ export function PackOpening({ packId }: PackOpeningProps) {
SELL FOR{" "}
{formatCurrency(
Math.floor(
- card.income *
+ resolveCardStats(card).income *
GAME_CONFIG.SELL_PRICE_MULTIPLIER,
),
)}
diff --git a/src/components/game/PortalArea.tsx b/src/components/game/PortalArea.tsx
index 7036f36..fd8ea96 100644
--- a/src/components/game/PortalArea.tsx
+++ b/src/components/game/PortalArea.tsx
@@ -7,9 +7,8 @@ export function PortalArea() {
const inventory = useGameStore((s) => s.inventory);
const toggleSlot = useGameStore((s) => s.toggleSlot);
- const inactiveCards = inventory.filter(
- (c) => !activeSlots.some((s) => s?.id === c.id),
- ).length;
+ const activeCount = activeSlots.filter(Boolean).length;
+ const inactiveCards = Math.max(0, (inventory.length - activeCount));
return (
@@ -19,8 +18,10 @@ export function PortalArea() {
Place cards to generate Mega Seeds • Collection bonus:{" "}
- +{inactiveCards}% (
- {inactiveCards} cards in inventory)
+
+ +{Math.max(0, Math.floor((inventory.length - activeCount) / 2))}%
+ {" "}
+ ({inactiveCards} cards in inventory)
diff --git a/src/config/gameConfig.ts b/src/config/gameConfig.ts
index 9e7c0c6..ee57965 100644
--- a/src/config/gameConfig.ts
+++ b/src/config/gameConfig.ts
@@ -4,9 +4,9 @@ export const GAME_CONFIG = {
INITIAL_SEEDS: 100,
INITIAL_MAX_INVENTORY: 50,
DIMENSION_ENTRY_COST: 1000,
- SELL_PRICE_MULTIPLIER: 100,
+ SELL_PRICE_MULTIPLIER: 0.8,
MAX_OFFLINE_SECONDS: 24 * 60 * 60, // 24h
-
+
UPGRADES: {
seeds: {
BASE_COST: 500,
@@ -21,11 +21,18 @@ export const GAME_CONFIG = {
JUMP_THRESHOLD: 10,
JUMP_MULTIPLIER: 5,
BONUS_PER_LEVEL: 0.05, // 5% per level
- }
+ },
+ inventory: {
+ BASE_COST: 1500,
+ COST_EXPONENT: 1.8,
+ JUMP_THRESHOLD: 10,
+ JUMP_MULTIPLIER: 4,
+ BONUS_PER_LEVEL: 4, // 10 slots per level
+ },
},
INCOME: {
- INACTIVE_CARD_BONUS: 0.01, // 1% per inactive card
+ INACTIVE_CARD_BONUS: 0.005, // 1% per inactive card
},
CARD_GENERATION: {
@@ -41,7 +48,7 @@ export const GAME_CONFIG = {
RARE: 0.3,
HOLO: 0.08,
FULL_ART: 0.02,
- }
+ },
},
DIMENSIONS: {
@@ -58,33 +65,11 @@ export const GAME_CONFIG = {
100: "Void Breach",
} as Record