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
3 changes: 2 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
13 changes: 7 additions & 6 deletions src/components/game/CollectionTab.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -62,7 +63,7 @@ export function CollectionTab() {

{displayCards.length > 0 ? (
<div className="flex justify-center gap-6 flex-wrap">
{displayCards.map((card) => {
{displayCards.map(({ card }) => {
const isInSlot = activeSlots.some((s) => s?.id === card.id);
return (
<div key={card.id} className="animate-in fade-in slide-in-from-bottom-2 duration-300">
Expand Down
66 changes: 42 additions & 24 deletions src/components/game/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,30 @@ 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;
onClick?: () => void;
isActive?: boolean;
}

const typeIcons: Record<string, any> = {
type RarityOrType =
| "COMMON"
| "NORMAL"
| "RARE"
| "HOLO"
| "FULL_ART"
| "SILVER"
| "GOLD"
| "REVERT"
| "SWORD";

const typeIcons = {
COMMON: Circle,
NORMAL: Circle,
RARE: Star,
Expand All @@ -28,9 +41,9 @@ const typeIcons: Record<string, any> = {
GOLD: Trophy,
REVERT: RefreshCw,
SWORD: Sword,
};
} satisfies Record<RarityOrType, LucideIcon>;

const rarityConfig: Record<string, any> = {
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" },
Expand All @@ -39,15 +52,17 @@ const rarityConfig: Record<string, any> = {
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<string, { border: string; bg: string }>;

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")
Expand All @@ -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
Expand Down Expand Up @@ -106,15 +119,15 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
{isFullArt ? (
<img
src={imgSrc}
alt={card.characterName}
alt={character.name}
className="w-full h-full object-cover transition-all duration-500"
style={{ filter: imageFilter }}
/>
) : (
<div className="flex items-center justify-center h-full bg-muted/50">
<img
src={imgSrc}
alt={card.characterName}
alt={character.name}
className="w-24 h-24 rounded-full object-cover border-2 border-border group-hover:scale-110 transition-all duration-500"
style={{ filter: imageFilter }}
/>
Expand All @@ -124,14 +137,14 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
<div className="absolute top-2 right-2 z-20">
<span
className={`text-[8px] px-1.5 py-0.5 rounded-full font-bold uppercase ${
card.status === "Alive"
character.status === "Alive"
? "bg-green-500/20 text-green-400 border border-green-500/40"
: card.status === "Dead"
: character.status === "Dead"
? "bg-red-500/20 text-red-400 border border-red-500/40"
: "bg-gray-500/20 text-gray-400 border border-gray-500/40"
}`}
>
{card.status}
{character.status}
</span>
</div>
</div>
Expand All @@ -140,10 +153,10 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
className={`p-3 space-y-1 ${isFullArt ? "bg-background/80 backdrop-blur-sm" : ""}`}
>
<p className="font-display text-[10px] font-bold text-foreground truncate leading-tight">
{card.characterName}
{character.name}
</p>
<p className="text-[8px] text-muted-foreground uppercase tracking-widest">
<span>{card.origin}</span>
<span>{character.origin}</span>
</p>
<div className="flex items-center justify-between">
<div className="flex items-center -space-x-1">
Expand All @@ -156,19 +169,24 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
})}
</div>
<div className="flex flex-col items-end">
<span className="text-xs font-bold text-primary">
+{formatNumber(card.income)}/s
</span>
<div className="flex items-center gap-1">
<Leaf className="w-2.5 h-2.5 text-green-500 fill-green-500/20" />
<span className="text-xs font-bold text-primary">
{formatNumber(income)}/s
</span>
</div>
<div className="flex items-center gap-1">
<Sword className="w-2.5 h-2.5 text-red-500 fill-red-500/20" />
<span className="text-[10px] font-bold text-red-500">
{formatNumber(card.power)}
{formatNumber(power)}
</span>
</div>
</div>
</div>
<div className="pt-1 border-t border-border/50 flex items-center justify-between opacity-60">
<span className="text-[8px] font-body uppercase">{card.species}</span>
<span className="text-[8px] font-body uppercase">
{character.species}
</span>
</div>
</div>
</button>
Expand Down
9 changes: 4 additions & 5 deletions src/components/game/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
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);
const activeSlots = useGameStore((s) => s.activeSlots);
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);
Expand Down
9 changes: 5 additions & 4 deletions src/components/game/PackOpening.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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.`,
});
};
Expand Down Expand Up @@ -300,7 +301,7 @@ export function PackOpening({ packId }: PackOpeningProps) {
SELL FOR{" "}
{formatCurrency(
Math.floor(
card.income *
resolveCardStats(card).income *
GAME_CONFIG.SELL_PRICE_MULTIPLIER,
),
)}
Expand Down
11 changes: 6 additions & 5 deletions src/components/game/PortalArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<section className="py-8 px-4">
Expand All @@ -19,8 +18,10 @@ export function PortalArea() {
</h2>
<p className="text-sm text-muted-foreground font-body">
Place cards to generate Mega Seeds • Collection bonus:{" "}
<span className="text-primary font-bold">+{inactiveCards}%</span> (
{inactiveCards} cards in inventory)
<span className="text-primary font-bold">
+{Math.max(0, Math.floor((inventory.length - activeCount) / 2))}%
</span>{" "}
({inactiveCards} cards in inventory)
</p>
</div>

Expand Down
47 changes: 16 additions & 31 deletions src/config/gameConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: {
Expand All @@ -41,7 +48,7 @@ export const GAME_CONFIG = {
RARE: 0.3,
HOLO: 0.08,
FULL_ART: 0.02,
}
},
},

DIMENSIONS: {
Expand All @@ -58,33 +65,11 @@ export const GAME_CONFIG = {
100: "Void Breach",
} as Record<number, string>,
PACK_UNLOCKS: {
"standard": 0,
"mega": 10,
standard: 0,
mega: 10,
"silver-rift": 25,
"alchemists-portal": 50,
"void-breach": 100,
} as Record<string, number>
}
};

/**
* Calculates the current income per second based on the game state.
* @param state The current game state
* @returns Income per second
*/
export const calculateCurrentIncome = (state: Pick<GameState, "activeSlots" | "inventory" | "upgrades">) => {
const activeIncome = state.activeSlots.reduce(
(sum: number, slot: GameCard | null) => sum + (slot?.income ?? 0),
0,
);

const inactiveCards = state.inventory.filter(
(c: GameCard) => !state.activeSlots.some((s: GameCard | null) => s?.id === c.id),
).length;

const bonus = 1 + inactiveCards * GAME_CONFIG.INCOME.INACTIVE_CARD_BONUS;

const upgradeBonus = 1 + state.upgrades.seeds * GAME_CONFIG.UPGRADES.seeds.BONUS_PER_LEVEL;

return activeIncome * bonus * upgradeBonus;
} as Record<string, number>,
},
};
Loading
Loading