# Use the official RainbowKit starter
npm init @rainbow-me/rainbowkit@latest
# When prompted:
# ✔ What is your project named? … ronin-rumble
# ✔ Which template would you like to use? › Next.js (App Router)
# ✔ Which package manager would you like to use? › npm
cd ronin-rumble# Game-specific dependencies
npm install zustand immer
npm install socket.io-client socket.io
npm install framer-motion react-dnd react-dnd-html5-backend
npm install react-hot-toast react-countdown
npm install class-variance-authority clsx tailwind-merge
npm install @supabase/supabase-js
# Development dependencies
npm install -D @types/node concurrently// app/providers.tsx (modify the existing one from RainbowKit)
'use client';
import * as React from 'react';
import {
RainbowKitProvider,
getDefaultWallets,
getDefaultConfig,
Chain,
} from '@rainbow-me/rainbowkit';
import {
argentWallet,
trustWallet,
ledgerWallet,
} from '@rainbow-me/rainbowkit/wallets';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Define Ronin chains
const roninMainnet: Chain = {
id: 2020,
name: 'Ronin Mainnet',
iconUrl: 'https://roninchain.com/favicon.ico',
iconBackground: '#fff',
nativeCurrency: {
decimals: 18,
name: 'RON',
symbol: 'RON',
},
rpcUrls: {
default: {
http: ['https://api.roninchain.com/rpc'],
},
public: {
http: ['https://api.roninchain.com/rpc'],
},
},
blockExplorers: {
default: {
name: 'Ronin Explorer',
url: 'https://explorer.roninchain.com'
},
},
testnet: false,
};
const roninTestnet: Chain = {
id: 2021,
name: 'Ronin Testnet',
iconUrl: 'https://roninchain.com/favicon.ico',
iconBackground: '#fff',
nativeCurrency: {
decimals: 18,
name: 'RON',
symbol: 'RON',
},
rpcUrls: {
default: {
http: ['https://saigon-testnet.roninchain.com/rpc'],
},
public: {
http: ['https://saigon-testnet.roninchain.com/rpc'],
},
},
blockExplorers: {
default: {
name: 'Ronin Explorer',
url: 'https://saigon-explorer.roninchain.com'
},
},
testnet: true,
};
const { wallets } = getDefaultWallets();
const config = getDefaultConfig({
appName: 'Ronin Rumble',
projectId: 'YOUR_PROJECT_ID', // Get from https://cloud.walletconnect.com
wallets: [
...wallets,
{
groupName: 'Other',
wallets: [argentWallet, trustWallet, ledgerWallet],
},
],
chains: [
roninTestnet,
roninMainnet,
],
ssr: true,
});
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}ronin-rumble/ # RainbowKit starter base
├── app/
│ ├── layout.tsx # Already exists, keep RainbowKit setup
│ ├── page.tsx # Modify to game landing
│ ├── providers.tsx # Already exists, modify for Ronin
│ ├── lobby/
│ │ └── page.tsx # Game lobby
│ ├── match/
│ │ └── [id]/
│ │ └── page.tsx # Active game
│ ├── api/
│ │ ├── socket/
│ │ │ └── route.ts # WebSocket endpoint
│ │ └── game/
│ │ └── route.ts # Game API
│ └── globals.css # Extend with game styles
│
├── components/ # Add game components
│ ├── game/
│ │ ├── Board.tsx
│ │ ├── Card.tsx
│ │ ├── Shop.tsx
│ │ ├── Timer.tsx
│ │ ├── PlayerStats.tsx
│ │ └── OpponentList.tsx
│ ├── ui/ # Reusable UI
│ │ ├── GameButton.tsx
│ │ └── Modal.tsx
│ └── ConnectButton.tsx # RainbowKit connect (exists)
│
├── lib/
│ ├── game/ # Game logic
│ │ ├── engine.ts
│ │ ├── units.ts
│ │ └── abilities.ts
│ ├── socket/ # WebSocket
│ │ └── client.ts
│ └── blockchain/ # Web3 integration
│ ├── contracts.ts
│ ├── flare.ts
│ └── filecoin.ts
│
├── store/ # Zustand stores
│ ├── gameStore.ts
│ └── playerStore.ts
│
├── hooks/ # Custom hooks
│ ├── useGame.ts
│ ├── useSocket.ts
│ └── useContract.ts
│
├── server/ # Game server (separate)
│ ├── index.ts
│ └── game/
│ └── GameRoom.ts
│
├── contracts/ # Smart contracts
│ └── RoninRumble.sol
│
└── public/ # Assets
└── units/ # Unit images
'use client';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount } from 'wagmi';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { useState } from 'react';
export default function Home() {
const { address, isConnected } = useAccount();
const router = useRouter();
const [selectedStake, setSelectedStake] = useState<number>(0);
const handlePlay = () => {
if (!isConnected) {
// Will trigger RainbowKit modal
document.querySelector('[aria-label="Connect Wallet"]')?.click();
return;
}
router.push(`/lobby?stake=${selectedStake}`);
};
return (
<main className="min-h-screen bg-gradient-to-b from-gray-900 via-purple-900 to-gray-900">
{/* Header with RainbowKit Connect Button */}
<header className="flex justify-between items-center p-6">
<div className="flex items-center gap-2">
<span className="text-2xl">⚔️</span>
<h1 className="text-2xl font-bold text-white">Ronin Rumble</h1>
</div>
<ConnectButton />
</header>
{/* Hero Section */}
<div className="container mx-auto px-4 py-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<h2 className="text-6xl font-bold text-white mb-4">
Web3 Card
<span className="bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
{" "}Autobattler
</span>
</h2>
<p className="text-xl text-gray-300 mb-12">
10-minute matches • 6-player lobbies • Position-based combat
</p>
{/* Stake Selection */}
<div className="max-w-2xl mx-auto mb-8">
<h3 className="text-lg text-gray-400 mb-4">Choose Your Stakes</h3>
<div className="grid grid-cols-4 gap-4">
{[
{ value: 0, label: 'Free', color: 'bg-gray-600' },
{ value: 2, label: '2 RON', color: 'bg-green-600' },
{ value: 10, label: '10 RON', color: 'bg-blue-600' },
{ value: 50, label: '50 RON', color: 'bg-purple-600' },
].map((stake) => (
<button
key={stake.value}
onClick={() => setSelectedStake(stake.value)}
className={`
p-4 rounded-lg font-bold transition-all
${selectedStake === stake.value
? `${stake.color} scale-105 ring-2 ring-white`
: 'bg-gray-700 hover:bg-gray-600'
}
`}
>
<div className="text-2xl mb-1">
{stake.value === 0 ? '🎮' : '💰'}
</div>
<div>{stake.label}</div>
{stake.value > 0 && (
<div className="text-xs mt-1 text-gray-300">
Win: {stake.value * 4} RON
</div>
)}
</button>
))}
</div>
</div>
{/* Play Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handlePlay}
className="px-12 py-4 bg-gradient-to-r from-green-500 to-green-600 rounded-lg font-bold text-xl text-white hover:from-green-600 hover:to-green-700 transition-all"
>
{isConnected ? 'Enter Lobby' : 'Connect Wallet to Play'}
</motion.button>
</motion.div>
{/* Game Info */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mt-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-gray-800/50 backdrop-blur rounded-lg p-6 text-center"
>
<div className="text-3xl mb-2">⚡</div>
<div className="text-2xl font-bold text-purple-400">10-15</div>
<div className="text-gray-400">Minutes per Game</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-gray-800/50 backdrop-blur rounded-lg p-6 text-center"
>
<div className="text-3xl mb-2">🎴</div>
<div className="text-2xl font-bold text-blue-400">30</div>
<div className="text-gray-400">Unique Units</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gray-800/50 backdrop-blur rounded-lg p-6 text-center"
>
<div className="text-3xl mb-2">🎯</div>
<div className="text-2xl font-bold text-green-400">8</div>
<div className="text-gray-400">Board Positions</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-gray-800/50 backdrop-blur rounded-lg p-6 text-center"
>
<div className="text-3xl mb-2">🏆</div>
<div className="text-2xl font-bold text-yellow-400">Top 3</div>
<div className="text-gray-400">Win Rewards</div>
</motion.div>
</div>
</div>
</main>
);
}import { useAccount, useBalance, useContractRead, useContractWrite, usePrepareContractWrite } from 'wagmi';
import { parseEther } from 'viem';
import { GAME_CONTRACT_ABI, GAME_CONTRACT_ADDRESS } from '@/lib/blockchain/contracts';
import { useEffect } from 'react';
import useGameStore from '@/store/gameStore';
export function useGame() {
const { address, isConnected } = useAccount();
const { data: balance } = useBalance({ address });
// Join Queue Transaction
const { config: joinConfig } = usePrepareContractWrite({
address: GAME_CONTRACT_ADDRESS,
abi: GAME_CONTRACT_ABI,
functionName: 'joinQueue',
value: parseEther('10'), // 10 RON entry fee
});
const {
write: joinQueue,
isLoading: isJoining,
isSuccess: joinSuccess
} = useContractWrite(joinConfig);
// Get current match
const { data: currentMatch } = useContractRead({
address: GAME_CONTRACT_ADDRESS,
abi: GAME_CONTRACT_ABI,
functionName: 'getPlayerMatch',
args: [address],
watch: true,
});
// Update store when match found
useEffect(() => {
if (currentMatch) {
useGameStore.getState().setMatch(currentMatch);
}
}, [currentMatch]);
return {
address,
isConnected,
balance: balance?.formatted,
joinQueue,
isJoining,
currentMatch,
};
}import {
useContractRead,
useContractWrite,
useWaitForTransaction,
usePrepareContractWrite
} from 'wagmi';
import { toast } from 'react-hot-toast';
export function useGameContract() {
// Claim rewards after match
const { config } = usePrepareContractWrite({
address: GAME_CONTRACT_ADDRESS,
abi: GAME_CONTRACT_ABI,
functionName: 'claimRewards',
args: [matchId],
});
const { data, write: claimRewards } = useContractWrite({
...config,
onSuccess: () => {
toast.success('Rewards claimed!');
},
onError: (err) => {
toast.error(`Failed: ${err.message}`);
},
});
const { isLoading, isSuccess } = useWaitForTransaction({
hash: data?.hash,
});
return {
claimRewards,
isLoading,
isSuccess,
};
}'use client';
import { useState, useEffect } from 'react';
import { useDrop } from 'react-dnd';
import { motion, AnimatePresence } from 'framer-motion';
import { Unit, Board as BoardType } from '@/types/game';
import { Card } from './Card';
import { useAccount } from 'wagmi';
interface BoardProps {
board: BoardType;
isPlayerBoard: boolean;
onCardDrop: (card: Unit, position: number) => void;
onCardClick: (position: number) => void;
isPlanning: boolean;
}
export function Board({
board,
isPlayerBoard,
onCardDrop,
onCardClick,
isPlanning
}: BoardProps) {
const { address } = useAccount();
const [highlightedPositions, setHighlightedPositions] = useState<number[]>([]);
const renderSlot = (card: Unit | null, position: number) => {
const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: 'card',
drop: (item: Unit) => onCardDrop(item, position),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
canDrop: () => isPlayerBoard && isPlanning,
}), [isPlayerBoard, isPlanning]);
return (
<motion.div
ref={drop}
key={position}
className={`
relative w-24 h-32 border-2 rounded-lg transition-all
${isOver && canDrop ? 'border-green-400 bg-green-400/20' : ''}
${!isOver && canDrop ? 'border-gray-600' : ''}
${!canDrop ? 'border-gray-700' : ''}
${highlightedPositions.includes(position) ? 'ring-2 ring-yellow-400' : ''}
`}
onClick={() => onCardClick(position)}
whileHover={isPlanning ? { scale: 1.05 } : {}}
>
<AnimatePresence mode="wait">
{card && (
<Card
unit={card}
isDraggable={isPlayerBoard && isPlanning}
position={position}
/>
)}
</AnimatePresence>
{/* Position Number */}
<div className="absolute -top-2 -left-2 w-6 h-6 bg-gray-800 rounded-full flex items-center justify-center text-xs font-bold">
{position + 1}
</div>
</motion.div>
);
};
return (
<div className="space-y-2">
{/* Board Label */}
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-400">
{isPlayerBoard ? 'Your Board' : 'Enemy Board'}
</span>
{isPlayerBoard && (
<span className="text-xs text-gray-500">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
)}
</div>
{/* Top Row (Positions 0-3) */}
<div className="flex gap-2 justify-center">
{[0, 1, 2, 3].map(i => renderSlot(board.top[i], i))}
</div>
{/* Bottom Row (Positions 4-7) */}
<div className="flex gap-2 justify-center">
{[0, 1, 2, 3].map(i => renderSlot(board.bottom[i], i + 4))}
</div>
</div>
);
}import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { Unit, Board, Player, Shop } from '@/types/game';
interface GameState {
// Match Info
matchId: string;
round: number;
phase: 'PLANNING' | 'COMBAT' | 'TRANSITION';
timeRemaining: number;
// Player
player: Player;
opponents: Player[];
currentOpponent?: Player;
// Shop & Board
shop: Shop;
board: Board;
bench: Unit[];
// Actions
buyCard: (index: number) => void;
sellCard: (unit: Unit) => void;
placeCard: (unit: Unit, position: number) => void;
rerollShop: () => void;
// Match Actions
setMatch: (match: any) => void;
updateFromServer: (data: any) => void;
}
const useGameStore = create<GameState>()(
immer((set) => ({
matchId: '',
round: 1,
phase: 'PLANNING',
timeRemaining: 20,
player: {
id: '',
address: '',
health: 20,
gold: 3,
level: 1,
xp: 0,
winStreak: 0,
loseStreak: 0,
},
opponents: [],
currentOpponent: undefined,
shop: {
cards: [],
rerollCost: 2,
freeRerolls: 0,
},
board: {
top: [null, null, null, null],
bottom: [null, null, null, null],
},
bench: [],
buyCard: (index) => set((state) => {
const card = state.shop.cards[index];
if (!card || state.player.gold < card.cost) return;
state.player.gold -= card.cost;
state.bench.push(card);
state.shop.cards.splice(index, 1);
}),
sellCard: (unit) => set((state) => {
state.player.gold += unit.cost;
// Remove from board
for (let i = 0; i < 4; i++) {
if (state.board.top[i]?.id === unit.id) {
state.board.top[i] = null;
return;
}
if (state.board.bottom[i]?.id === unit.id) {
state.board.bottom[i] = null;
return;
}
}
// Remove from bench
state.bench = state.bench.filter(u => u.id !== unit.id);
}),
placeCard: (unit, position) => set((state) => {
// Remove from current position
for (let i = 0; i < 4; i++) {
if (state.board.top[i]?.id === unit.id) state.board.top[i] = null;
if (state.board.bottom[i]?.id === unit.id) state.board.bottom[i] = null;
}
state.bench = state.bench.filter(u => u.id !== unit.id);
// Place at new position
if (position < 4) {
const existing = state.board.top[position];
if (existing) state.bench.push(existing);
state.board.top[position] = unit;
} else {
const existing = state.board.bottom[position - 4];
if (existing) state.bench.push(existing);
state.board.bottom[position - 4] = unit;
}
}),
rerollShop: () => set((state) => {
if (state.shop.freeRerolls > 0) {
state.shop.freeRerolls--;
} else if (state.player.gold >= state.shop.rerollCost) {
state.player.gold -= state.shop.rerollCost;
}
// Server will send new shop
}),
setMatch: (match) => set((state) => {
state.matchId = match.id;
state.opponents = match.players;
}),
updateFromServer: (data) => set((state) => {
Object.assign(state, data);
}),
}))
);
export default useGameStore;'use client';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount, useBalance } from 'wagmi';
import { motion } from 'framer-motion';
import { useGame } from '@/hooks/useGame';
import toast from 'react-hot-toast';
export default function LobbyPage() {
const router = useRouter();
const searchParams = useSearchParams();
const stake = Number(searchParams.get('stake')) || 0;
const { address, isConnected } = useAccount();
const { data: balance } = useBalance({ address });
const { joinQueue, isJoining, currentMatch } = useGame();
const [isQueuing, setIsQueuing] = useState(false);
const [playersInQueue, setPlayersInQueue] = useState(0);
useEffect(() => {
if (!isConnected) {
router.push('/');
}
}, [isConnected]);
useEffect(() => {
if (currentMatch) {
router.push(`/match/${currentMatch.id}`);
}
}, [currentMatch]);
const handleJoinQueue = async () => {
if (stake > 0 && balance && parseFloat(balance.formatted) < stake) {
toast.error(`Insufficient balance. Need ${stake} RON`);
return;
}
setIsQueuing(true);
if (stake > 0) {
// On-chain queue for paid games
await joinQueue?.();
} else {
// Off-chain queue for free games
// Connect to WebSocket server
}
};
return (
<div className="min-h-screen bg-gradient-to-b from-gray-900 to-gray-800">
{/* Header */}
<header className="flex justify-between items-center p-6">
<h1 className="text-2xl font-bold text-white">
⚔️ Ronin Rumble - Lobby
</h1>
<ConnectButton />
</header>
<div className="container mx-auto px-4 py-8">
{/* Queue Status */}
{isQueuing ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-20"
>
<div className="text-6xl mb-4">⚔️</div>
<h2 className="text-3xl font-bold mb-4">Finding Match...</h2>
<p className="text-xl text-gray-400 mb-8">
{playersInQueue}/6 Players Found
</p>
<div className="flex justify-center gap-2">
{[...Array(6)].map((_, i) => (
<div
key={i}
className={`w-12 h-12 rounded-full ${
i < playersInQueue ? 'bg-green-500' : 'bg-gray-700'
}`}
/>
))}
</div>
<button
onClick={() => setIsQueuing(false)}
className="mt-8 px-6 py-2 bg-red-600 hover:bg-red-700 rounded-lg"
>
Cancel Queue
</button>
</motion.div>
) : (
<div className="max-w-4xl mx-auto">
{/* Stake Info */}
<div className="bg-gray-800 rounded-lg p-6 mb-8">
<h2 className="text-2xl font-bold mb-4">Match Details</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-gray-400">Entry Fee:</span>
<span className="ml-2 font-bold">
{stake === 0 ? 'Free' : `${stake} RON`}
</span>
</div>
<div>
<span className="text-gray-400">Prize Pool:</span>
<span className="ml-2 font-bold">
{stake === 0 ? 'Practice' : `${stake * 6} RON`}
</span>
</div>
<div>
<span className="text-gray-400">Your Balance:</span>
<span className="ml-2 font-bold">
{balance?.formatted || '0'} {balance?.symbol}
</span>
</div>
<div>
<span className="text-gray-400">Players:</span>
<span className="ml-2 font-bold">6</span>
</div>
</div>
</div>
{/* Rewards */}
<div className="bg-gray-800 rounded-lg p-6 mb-8">
<h3 className="text-xl font-bold mb-4">Rewards</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span>🥇 1st Place:</span>
<span className="font-bold text-yellow-400">
{stake === 0 ? 'Glory' : `${stake * 4} RON`}
</span>
</div>
<div className="flex justify-between">
<span>🥈 2nd Place:</span>
<span className="font-bold text-gray-400">
{stake === 0 ? 'Honor' : `${stake} RON`}
</span>
</div>
<div className="flex justify-between">
<span>🥉 3rd Place:</span>
<span className="font-bold text-orange-600">
{stake === 0 ? 'Experience' : `${stake * 0.5} RON`}
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-4">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleJoinQueue}
disabled={isJoining}
className="flex-1 py-4 bg-gradient-to-r from-green-500 to-green-600 rounded-lg font-bold text-xl disabled:opacity-50"
>
{isJoining ? 'Joining...' : 'Find Match'}
</motion.button>
<button
onClick={() => router.push('/')}
className="px-8 py-4 bg-gray-700 hover:bg-gray-600 rounded-lg font-bold"
>
Back
</button>
</div>
</div>
)}
</div>
</div>
);
}{
"name": "ronin-rumble",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"server": "tsx watch server/index.ts",
"dev:all": "concurrently \"npm run dev\" \"npm run server\""
},
"dependencies": {
"@rainbow-me/rainbowkit": "^2.0.0",
"@tanstack/react-query": "^5.0.0",
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"viem": "2.x",
"wagmi": "^2.5.0",
"zustand": "^4.4.7",
"immer": "^10.0.3",
"socket.io": "^4.6.0",
"socket.io-client": "^4.6.0",
"framer-motion": "^10.16.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-hot-toast": "^2.4.1",
"react-countdown": "^2.3.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.2.0",
"@supabase/supabase-js": "^2.39.0",
"web3.storage": "^4.5.5"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"concurrently": "^8.2.2",
"tsx": "^4.7.0"
}
}# Step 1: Create project with RainbowKit
npm init @rainbow-me/rainbowkit@latest
# Choose: ronin-rumble, Next.js App Router, npm
# Step 2: Install game dependencies
cd ronin-rumble
npm install zustand immer socket.io-client socket.io framer-motion react-dnd react-dnd-html5-backend react-hot-toast react-countdown class-variance-authority clsx tailwind-merge @supabase/supabase-js web3.storage
# Step 3: Install dev dependencies
npm install -D concurrently tsx
# Step 4: Start development
npm run dev
# Step 5: In another terminal, start game server
npm run server- Wallet Connection: RainbowKit handles all wallet UI/UX
- Wagmi Hooks: Use
useAccount,useBalance,useContractWriteinstead of raw ethers - Provider Setup: RainbowKit provides the providers wrapper
- TypeScript Config: Already configured properly
- Styling: RainbowKit comes with base styles we extend
✅ Professional wallet UI out of the box
✅ Multiple wallet support (MetaMask, WalletConnect, Coinbase, etc.)
✅ Account management handled automatically
✅ Network switching UI included
✅ ENS support built-in
✅ Mobile responsive wallet connection
✅ TypeScript fully configured
✅ Best practices for Web3 UX
This approach gives you a production-ready Web3 foundation to build the game on top of!