/** * CoinRush Barbarian Arena - React Version * Turn-based PvP duel game with premium UI * VERSION: 43 - Fixed view button for open battles */ // Log version on load to verify browser cache console.log('๐Ÿฐ Barbarian Arena v43 loaded - VIEW BUTTON FIX'); const { useState, useEffect, useRef, useCallback } = React; // ============================================ // CONFIG // ============================================ const CONFIG = { minStake: 1, maxStake: 100000, battleCountdown: 15000, turnTimer: 15000, wsReconnectDelay: 3000 }; // Attack types with stats const ATTACKS = { quick: { name: 'Quick Strike', hitChance: 85, damage: [18, 24], icon: 'quickStrike', color: '#3b82f6', desc: 'High accuracy, low damage' }, normal: { name: 'Normal Attack', hitChance: 70, damage: [22, 30], icon: 'normalAttack', color: '#f59e0b', desc: 'Balanced risk and reward' }, hard: { name: 'Heavy Smash', hitChance: 50, damage: [32, 40], icon: 'heavySmash', color: '#ef4444', desc: 'High risk, high reward' } }; // Potion config (separate from attacks) const POTION = { name: 'Health Potion', heal: [20, 30], icon: 'potion', color: '#22c55e', desc: 'Restore 20-30 HP (1 per battle)' }; // ============================================ // SVG ICONS // ============================================ const Icons = { sword: ( ), shield: ( ), trophy: ( ), users: ( ), coin: ( ), clock: ( ), play: ( ), eye: ( ), plus: ( ), x: ( ), heart: ( ), zap: ( ), volumeOn: ( ), volumeOff: ( ), info: ( ), // Attack icons quickStrike: ( ), normalAttack: ( ), heavySmash: ( ), // Health potion icon potion: ( ), // Dice icon dice: ( ), // Trophy icon trophy: ( ), // Crown icon for winner crown: ( ), // Skull icon for loser skull: ( ), // Timer/Clock icon timer: ( ), // Heart icon for HP heart: ( ), // Close/X icon close: ( ), // VS icon versus: ( VS ), // Coins icon coins: ( ), // Plus icon plus: ( ), // Bot icon bot: ( ), // Arrow icon arrow: ( ), // Target icon target: ( ), // Refresh icon refresh: ( ), // Eye icon for viewing eye: ( ), // Resume/Play icon play: ( ), // Daily Rewards Modal Icons flame: ( ), gift: ( ), slotMachine: ( ), star: ( ), spinner: ( ), lock: ( ), check: ( ), swords: ( ) }; // ============================================ // DAILY REWARDS HELPERS // ============================================ // Helper to get SVG icon for reward type const getRewardIcon = (type) => { const typeIcons = { 'xp_boost': Icons.zap, 'coinflip_discount': Icons.target, 'barbarian_discount': Icons.swords, 'jackpot_discount': Icons.slotMachine, 'combo': Icons.star }; if (typeIcons[type]) return typeIcons[type]; return Icons.gift; }; // Get reward color based on type const getRewardColor = (type) => { const colors = { 'xp_boost': { primary: '#ffd700', secondary: '#ff9500', gradient: 'linear-gradient(135deg, #ffd700, #ff9500)' }, 'coinflip_discount': { primary: '#3b82f6', secondary: '#1d4ed8', gradient: 'linear-gradient(135deg, #3b82f6, #1d4ed8)' }, 'barbarian_discount': { primary: '#ef4444', secondary: '#dc2626', gradient: 'linear-gradient(135deg, #ef4444, #dc2626)' }, 'jackpot_discount': { primary: '#a855f7', secondary: '#7c3aed', gradient: 'linear-gradient(135deg, #a855f7, #7c3aed)' }, 'combo': { primary: '#4fffb0', secondary: '#2dd490', gradient: 'linear-gradient(135deg, #4fffb0, #2dd490)' } }; return colors[type] || { primary: '#4fffb0', secondary: '#2dd490', gradient: 'linear-gradient(135deg, #4fffb0, #2dd490)' }; }; // ============================================ // UTILITY FUNCTIONS // ============================================ function formatCoins(hundredths) { const coins = hundredths / 100; return coins.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } function generatePlayerColor(username) { const colors = [ { bg: 'linear-gradient(135deg, #f97316, #facc15)', solid: '#f97316' }, { bg: 'linear-gradient(135deg, #ef4444, #f97316)', solid: '#ef4444' }, { bg: 'linear-gradient(135deg, #8b5cf6, #ec4899)', solid: '#8b5cf6' }, { bg: 'linear-gradient(135deg, #3b82f6, #06b6d4)', solid: '#3b82f6' }, { bg: 'linear-gradient(135deg, #10b981, #34d399)', solid: '#10b981' }, { bg: 'linear-gradient(135deg, #f59e0b, #fbbf24)', solid: '#f59e0b' } ]; // Handle null/undefined username if (!username) { return { bg: 'linear-gradient(135deg, #666, #888)', solid: '#666' }; } let hash = 0; for (let i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } function getTimeRemaining(expiresAt) { const now = Date.now(); const expires = new Date(expiresAt).getTime(); const remaining = Math.max(0, expires - now); const mins = Math.floor(remaining / 60000); const secs = Math.floor((remaining % 60000) / 1000); return { mins, secs, total: remaining }; } // ============================================ // AVATAR COMPONENT // ============================================ function Avatar({ username, size = 'md', showBorder = true, level = 1, showLevel = false }) { // Size classes const sizeClasses = { xs: 'cf-avatar--xs', sm: 'cf-avatar--sm', md: 'cf-avatar--md', lg: 'cf-avatar--lg', xl: 'cf-avatar--xl' }; // Border frame sizes const frameSizes = { xs: 44, sm: 72, md: 86, lg: 100, xl: 120 }; // Background color const getBackground = () => { if (typeof window.colorFrom === 'function') { return window.colorFrom(username || 'X'); } const hue = [...(username || 'X')].reduce((a, c) => a + c.charCodeAt(0), 0) % 360; return `linear-gradient(135deg, hsl(${hue} 70% 50%), hsl(${hue} 70% 35%))`; }; // Border frame image const getBorderFrame = () => { if (level >= 40) return '/static/img/borders/Challengerborder.png'; if (level >= 30) return '/static/img/borders/Diamondborder.png'; if (level >= 20) return '/static/img/borders/Rubyborder.png'; if (level >= 15) return '/static/img/borders/Goldborder.png'; if (level >= 10) return '/static/img/borders/Silverborder.png'; if (level >= 5) return '/static/img/borders/Bronzeborder.png'; return null; }; // Border class const getBorderClass = () => { if (typeof window.getAvatarBorderClass === 'function') { return window.getAvatarBorderClass(level); } return ''; }; const borderClass = showBorder ? getBorderClass() : ''; const frameUrl = showBorder ? getBorderFrame() : null; const frameSize = frameSizes[size] || 86; return (
{(username || 'X').slice(0, 2).toUpperCase()} {showLevel && level > 0 && ( {level} )} {frameUrl && ( )}
); } // ============================================ // CREATE DUEL PANEL // ============================================ function CreateDuelPanel({ user, onCreateDuel, isCreating }) { const [amount, setAmount] = useState(''); const [soundEnabled, setSoundEnabled] = useState(true); const handleCreate = () => { const stake = parseInt(amount, 10); if (stake >= CONFIG.minStake && user) { onCreateDuel(stake); } }; const adjustAmount = (delta) => { const current = parseInt(amount, 10) || 0; setAmount(Math.max(0, current + delta).toString()); }; const setMax = () => { if (user) { setAmount(Math.floor(user.balance / 100).toString()); } }; const canCreate = user && parseInt(amount, 10) >= CONFIG.minStake && !isCreating; return (
{Icons.sword}

CREATE DUEL

Set your stake and challenge warriors

setAmount(e.target.value)} min={CONFIG.minStake} />
); } // ============================================ // BATTLE CARD // ============================================ function BattleCard({ battle, user, onJoin, onView, isJoining }) { const [timeLeft, setTimeLeft] = useState(300); // 5 minutes in seconds const isOwn = user?.username === battle.creator; const isOpponent = battle.opponent && user?.username === battle.opponent; const isParticipant = isOwn || isOpponent; const isResolved = battle.status === 'resolved'; const isActive = battle.status === 'active'; const canJoin = user && battle.status === 'open' && !isOwn; // Countdown timer for open battles useEffect(() => { if (battle.status !== 'open') return; let createdAtStr = battle.created_at; if (createdAtStr && !createdAtStr.endsWith('Z') && !createdAtStr.includes('+')) { createdAtStr += 'Z'; } const createdAt = new Date(createdAtStr).getTime(); if (!Number.isFinite(createdAt)) return; const updateTimer = () => { const elapsed = (Date.now() - createdAt) / 1000; const remaining = Math.max(0, 300 - elapsed); // 5 min timeout setTimeLeft(remaining); }; updateTimer(); const interval = setInterval(updateTimer, 1000); return () => clearInterval(interval); }, [battle.created_at, battle.status]); const isUrgent = timeLeft < 60; const isWarning = timeLeft < 120 && timeLeft >= 60; const winnerIsCreator = isResolved && battle.winner === battle.creator; const formatTime = (secs) => { const m = Math.floor(secs / 60); const s = Math.floor(secs % 60); return `${m}:${s.toString().padStart(2, '0')}`; }; return (
{/* Main row with players */}
{/* Creator */}
{battle.creator} {formatCoins(battle.amount)}
{isResolved && winnerIsCreator && {Icons.crown}}
{/* VS */}
VS
{/* Opponent or waiting */}
{battle.opponent ? ( <> {isResolved && !winnerIsCreator && {Icons.crown}}
{battle.opponent} {formatCoins(battle.join_amount || battle.amount)}
) : (
? Waiting...
)}
{/* Footer */}
{formatCoins(battle.total_pot || battle.amount * 2)}
{/* Status */}
{isResolved ? ( <>{battle.winner} WINS ) : isActive ? ( <> LIVE ) : ( <>{Icons.clock} {formatTime(timeLeft)} )}
{/* Actions */}
{/* Eye button - always visible for spectating/viewing */} {/* Join button - for open battles that aren't your own */} {battle.status === 'open' && !isOwn && ( )} {/* Resume button - for active battles you're in */} {isActive && isParticipant && ( )}
); } // ============================================ // LIVE DUELS LIST // ============================================ function LiveDuelsList({ battles, user, onJoin, onView, isLoading, isJoining }) { const openBattles = battles.filter(b => b.status === 'open'); const activeBattles = battles.filter(b => b.status === 'active'); const recentResolved = battles.filter(b => b.status === 'resolved').slice(0, 5); if (isLoading) { return (
{Icons.trophy}

LIVE DUELS

Loading battles...
); } const allBattles = [...activeBattles, ...openBattles, ...recentResolved]; return (
{Icons.trophy}

LIVE DUELS

{activeBattles.length > 0 && ( {activeBattles.length} Active )} {openBattles.length > 0 && ( {openBattles.length} Open )}
{allBattles.length === 0 ? (
{Icons.trophy}

No Active Duels

Create the first duel to start battling!

) : ( allBattles.map(battle => ( )) )}
); } // ============================================ // FLOATING DAMAGE TEXT COMPONENT // ============================================ function FloatingDamage({ damage, isHit, targetSide, isHeal }) { const [visible, setVisible] = useState(true); useEffect(() => { const timer = setTimeout(() => setVisible(false), 1500); return () => clearTimeout(timer); }, []); if (!visible) return null; // Heal effect: green positive number if (isHeal) { return (
+{damage}
); } return (
{isHit ? `-${damage}` : 'MISS!'}
); } // ============================================ // BLOOD SPLATTER COMPONENT // ============================================ function BloodSplatter({ side, intensity }) { const [particles, setParticles] = useState([]); useEffect(() => { // Generate random blood particles const count = Math.min(intensity / 5, 8) + 3; const newParticles = []; for (let i = 0; i < count; i++) { newParticles.push({ id: i, x: (Math.random() - 0.5) * 60, y: (Math.random() - 0.5) * 40, size: Math.random() * 8 + 4, delay: Math.random() * 0.2, duration: 0.5 + Math.random() * 0.3 }); } setParticles(newParticles); // Clean up after animation const timer = setTimeout(() => setParticles([]), 1500); return () => clearTimeout(timer); }, [intensity]); return (
{particles.map(p => (
))}
); } // ============================================ // BATTLE OVERLAY (Duel Arena) // ============================================ function BattleOverlay({ battle, user, onClose, onAttack, isMyTurn, battleLog, countdown, turnTimer, diceRoll, battlePhase, damageEffect, attackAnimation }) { if (!battle) return null; const isCreator = user?.username === battle.creator; const isOpponent = user?.username === battle.opponent; const isParticipant = isCreator || isOpponent; const isSpectator = !isParticipant; const myRole = isCreator ? 'creator' : 'opponent'; const myHP = isCreator ? battle.creator_hp : battle.opponent_hp; const opponentHP = isCreator ? battle.opponent_hp : battle.creator_hp; const myName = isCreator ? battle.creator : battle.opponent; const opponentName = isCreator ? battle.opponent : battle.creator; const myColor = generatePlayerColor(myName || 'You'); const opponentColor = generatePlayerColor(opponentName || 'Opponent'); const isBattleComplete = battle.status === 'resolved'; const isWinner = battle.winner === user?.username; // Determine sprite states based on animations const getCreatorSprite = () => { if (damageEffect?.target === 'creator' && damageEffect.isHeal) return 'potion'; // Drinking animation for heal if (damageEffect?.target === 'creator' && !damageEffect.isHeal) return 'hurt'; if (attackAnimation?.attacker === 'creator') return attackAnimation.type; return 'idle'; }; const getOpponentSprite = () => { if (damageEffect?.target === 'opponent' && damageEffect.isHeal) return 'potion'; // Drinking animation for heal if (damageEffect?.target === 'opponent' && !damageEffect.isHeal) return 'hurt'; if (attackAnimation?.attacker === 'opponent') return attackAnimation.type; return 'idle'; }; const creatorSprite = getCreatorSprite(); const opponentSprite = getOpponentSprite(); // Check if we're in countdown phase (show countdown in center instead of VS) const isCountdownPhase = battlePhase === 'countdown'; return (
{/* Close button - always visible */} {/* Arena Header */}

{isBattleComplete ? ( <>{Icons.trophy} BATTLE COMPLETE ) : (isCountdownPhase || battlePhase === 'waiting') ? ( <>{Icons.sword} GET READY ) : ( <>{Icons.sword} BATTLE IN PROGRESS )}

Total Pot: {formatCoins(battle.total_pot || battle.amount * 2)} coins
{/* Unified Battle Arena */}
{/* Fighter Stats - Left (ALWAYS CREATOR) */}
{battle.creator} {isCreator && YOU}
{Icons.heart} {battle.creator_hp || 0} HP
{/* Battle Stage - Both fighters in same box */}
{/* Left Fighter Sprite */}
{damageEffect?.target === 'creator' && damageEffect.hit && !damageEffect.isHeal && ( )} {damageEffect?.target === 'creator' && ( )}
{/* Center: VS badge, countdown, or loading */} {battlePhase === 'waiting' ? (
) : isCountdownPhase ? (
{countdown}
{diceRoll?.firstPlayer && (
{diceRoll.firstPlayer} attacks first!
)}
) : (
VS
)} {/* Right Fighter Sprite */}
{damageEffect?.target === 'opponent' && damageEffect.hit && !damageEffect.isHeal && ( )} {damageEffect?.target === 'opponent' && ( )}
{/* Fighter Stats - Right (ALWAYS OPPONENT) */}
{battle.opponent || 'Waiting...'} {isOpponent && YOU}
{Icons.heart} {battle.opponent_hp || 0} HP
{/* Battle Controls */} {!isBattleComplete && (
{/* Fighting Phase - Your Turn */} {battlePhase === 'fighting' && isParticipant && isMyTurn && ( <>
YOUR TURN {turnTimer > 0 && ( {turnTimer}s )}
{Object.entries(ATTACKS).map(([key, attack]) => ( ))}
{/* Potion Button - Side panel */} {(() => { const myPotionUsed = isCreator ? battle.creator_potion_used : battle.opponent_potion_used; return ( ); })()}
Press 1 2 3 to attack โ€ข 4 for potion
)} {/* Phase 3: Fighting - Waiting for opponent */} {battlePhase === 'fighting' && isParticipant && !isMyTurn && (
Waiting for opponent... {turnTimer > 0 && ( {turnTimer}s )}
)} {/* Spectator view */} {isSpectator && (
{Icons.eye} Spectating
)}
)} {/* Victory/Defeat Banner */} {isBattleComplete && (
{isWinner ? Icons.trophy : isSpectator ? Icons.sword : Icons.skull}
{isWinner ? 'VICTORY!' : isSpectator ? `${battle.winner} Wins!` : 'DEFEAT'}
{isWinner && (
+{formatCoins(battle.total_pot * 0.975)} coins
)}
)} {/* Battle Log */}
Battle Log
{battleLog.map((entry, i) => (
{entry.message}
))}
); } // ============================================ // RULES MODAL // ============================================ function RulesModal({ isOpen, onClose }) { const [activeTab, setActiveTab] = useState('basics'); if (!isOpen) return null; return (
e.stopPropagation()}> {/* Header */}
{Icons.sword}

Knight Duel Rules

Master the art of combat and claim victory!

{/* Tab Navigation */}
{/* Tab Content */}
{/* BASICS TAB */} {activeTab === 'basics' && (
1

Create or Join a Duel

Set your stake amount and create a new duel, or browse existing duels and join one that matches your budget.

2

Wait for an Opponent

Your duel will appear in the lobby. When someone joins, a dice roll determines who attacks first!

3

Battle to Victory

Take turns attacking until one knight's HP reaches 0. Each turn has a 15-second timer - act fast!

{Icons.shield}
Both knights start with 100 HP Reduce your opponent's health to zero to win!
)} {/* COMBAT TAB */} {activeTab === 'combat' && (

Choose your attack wisely! Each attack has different hit chance and damage.

{Icons.quickStrike}

Quick Strike

85% Hit 18-24 DMG

High accuracy, lower damage. Safe and reliable.

{Icons.normalAttack}

Normal Attack

70% Hit 22-30 DMG

Balanced risk and reward. The standard choice.

{Icons.heavySmash}

Heavy Smash

50% Hit 32-40 DMG

High risk, high reward. Miss more, but hit harder!

โŒจ๏ธ TIP

Use keyboard shortcuts 1 2 3 for quick attacks!

)} {/* ITEMS TAB */} {activeTab === 'items' && (

Each knight has access to special items during battle.

Health Potion

Health Potion

+20-30 HP
  • Restores 20-30 health points
  • Can only be used once per battle
  • Does NOT end your turn - attack after healing!
  • Press 4 or click the potion button
๐Ÿ’ก Strategy Tips
  • Save your potion for when you're low on HP
  • Using potion + attack in one turn can turn the tide!
  • Don't waste it early - you only get one!
)} {/* REWARDS TAB */} {activeTab === 'rewards' && (
{Icons.trophy}

Winner Takes All

The victorious knight claims the entire prize pool!

Prize Calculation

Your Stake + Opponent's Stake
Total Pot = Combined Stakes
House Fee - 2.5%
Your Prize = 97.5% of Total Pot
๐Ÿ“Š Example

You stake 100 coins, opponent stakes 100 coins

Total pot: 200 coins

Winner receives: 195 coins (after 2.5% fee)

โฑ๏ธ
Turn Timer

You have 15 seconds per turn. If time runs out, a random attack is made for you!

)}
{/* Footer */}
); } // ============================================ // DAILY REWARD MODAL // ============================================ function DailyRewardModal({ isOpen, onClose, user, onClaim }) { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [claiming, setClaiming] = useState(false); const [claimResult, setClaimResult] = useState(null); const [showConfetti, setShowConfetti] = useState(false); // Fetch status when modal opens useEffect(() => { if (isOpen && user) { setLoading(true); setClaimResult(null); fetch('/api/daily-reward/status', { credentials: 'include' }) .then(res => res.ok ? res.json() : null) .then(data => { setStatus(data); setLoading(false); }) .catch(() => setLoading(false)); } }, [isOpen, user]); const handleClaim = async () => { if (claiming || !status || status.daily_reward_claimed) return; setClaiming(true); try { const res = await fetch('/api/daily-reward/claim', { method: 'POST', credentials: 'include' }); const data = await res.json(); if (res.ok) { setClaimResult(data); setShowConfetti(true); setStatus(prev => ({ ...prev, daily_reward_claimed: true })); onClaim?.(data); setTimeout(() => setShowConfetti(false), 3000); } else { setClaimResult({ error: data.detail || 'Failed to claim reward' }); } } catch (e) { setClaimResult({ error: 'Network error' }); } setClaiming(false); }; if (!isOpen) return null; const streak = status?.login_streak || 1; const reward = status?.today_reward || {}; const claimed = status?.daily_reward_claimed || false; const rewardColor = getRewardColor(reward.type); // Get visible days for calendar (show 7 days centered on current) const allRewards = status?.all_rewards || {}; const visibleDays = []; const startDay = Math.max(1, streak - 3); const endDay = Math.min(30, startDay + 6); for (let i = startDay; i <= endDay; i++) { visibleDays.push({ day: i, reward: allRewards[i] || {}, isCurrent: i === streak, isPast: i < streak, isFuture: i > streak }); } // Check if there are any active bonuses const hasActiveBonuses = status?.active_bonuses && ( status.active_bonuses.xp_boost > 0 || status.active_bonuses.coinflip_discounts > 0 || status.active_bonuses.barbarian_discounts > 0 || status.active_bonuses.jackpot_discounts > 0 ); return (
e.stopPropagation()}> {/* Confetti Effect */} {showConfetti && (
{[...Array(60)].map((_, i) => (
))}
)} {/* Decorative Background */}
{/* Close Button */} {/* Content */}
{loading ? (
{Icons.spinner}
Loading rewards...
) : !user ? (
{Icons.lock}

Login Required

Please login to claim your daily rewards!

) : ( <> {/* Hero Section */}
{getRewardIcon(reward.type)}
{Icons.flame}
{streak} Day Streak

{claimed ? 'Reward Claimed!' : 'Daily Reward'}

{claimed ? 'Come back tomorrow to continue your streak!' : 'Claim your reward and keep your streak alive!' }

{/* Today's Reward Card */}
Day {streak}
{getRewardIcon(reward.type)}

{reward.name || 'Daily Reward'}

{reward.description || 'Claim your daily reward!'}

{claimed && (
{Icons.check}
)}
{/* Calendar Timeline */}
Reward Calendar
{visibleDays.map(({ day, reward: dayReward, isCurrent, isPast, isFuture }) => { const dayColor = getRewardColor(dayReward.type); return (
Day {day}
{getRewardIcon(dayReward.type)}
{isPast &&
{Icons.check}
}
); })}
{/* Active Bonuses (if any) */} {hasActiveBonuses && (
Active Power-Ups
{status.active_bonuses.xp_boost > 0 && (
{Icons.zap} +{status.active_bonuses.xp_boost}% XP
)} {status.active_bonuses.coinflip_discounts > 0 && (
{Icons.target} {status.active_bonuses.coinflip_discounts}x Flip
)} {status.active_bonuses.barbarian_discounts > 0 && (
{Icons.swords} {status.active_bonuses.barbarian_discounts}x Duel
)} {status.active_bonuses.jackpot_discounts > 0 && (
{Icons.slotMachine} {status.active_bonuses.jackpot_discounts}x Jackpot
)}
)} {/* Claim Button */}
{claimed ? (
{Icons.clock} Next reward in Tomorrow
) : ( )} {claimResult?.error && (
{claimResult.error}
)}
)}
); } // ============================================ // MAIN APP // ============================================ function BarbarianApp() { const [user, setUser] = useState(null); const [battles, setBattles] = useState([]); const [history, setHistory] = useState([]); // Recent battle history for ticker const [isConnected, setIsConnected] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isCreating, setIsCreating] = useState(false); const [isJoining, setIsJoining] = useState(false); // Prevent double-clicks on join const [showRules, setShowRules] = useState(false); const [showDailyModal, setShowDailyModal] = useState(false); const [showSocialPanel, setShowSocialPanel] = useState(false); const [activeBattle, setActiveBattle] = useState(null); const [isMyTurn, setIsMyTurn] = useState(false); const [battleLog, setBattleLog] = useState([]); const [countdown, setCountdown] = useState(0); const [turnTimer, setTurnTimer] = useState(0); const [showAuthModal, setShowAuthModal] = useState(false); const [authMode, setAuthMode] = useState('login'); const [diceRoll, setDiceRoll] = useState(null); // { rolling: bool, result: 'creator'|'opponent', firstPlayer: string } const [battlePhase, setBattlePhase] = useState('waiting'); // 'waiting' | 'dice_roll' | 'countdown' | 'fighting' const [damageEffect, setDamageEffect] = useState(null); // { target: 'creator'|'opponent', damage: number, hit: bool } const [attackAnimation, setAttackAnimation] = useState(null); // { attacker: 'creator'|'opponent', type: 'quick'|'normal'|'hard' } const wsRef = useRef(null); const userRef = useRef(null); const turnTimerRef = useRef(null); const battlePhaseRef = useRef(null); const isMyTurnRef = useRef(false); // Keep ref in sync useEffect(() => { userRef.current = user; }, [user]); useEffect(() => { battlePhaseRef.current = battlePhase; }, [battlePhase]); useEffect(() => { isMyTurnRef.current = isMyTurn; }, [isMyTurn]); // Expose openAuthModal to window useEffect(() => { window.openAuthModal = (mode = 'login') => { setAuthMode(mode); setShowAuthModal(true); }; return () => { delete window.openAuthModal; }; }, []); // Expose dailyRewardsModal to window for navbar useEffect(() => { window.dailyRewardsModal = { open: () => setShowDailyModal(true), close: () => setShowDailyModal(false) }; return () => { delete window.dailyRewardsModal; }; }, []); // Expose rules modal to window useEffect(() => { window.showBarbarianRules = () => setShowRules(true); return () => { delete window.showBarbarianRules; }; }, []); // Expose social panel toggle to window useEffect(() => { window.toggleSocialPanel = () => setShowSocialPanel(prev => !prev); return () => { delete window.toggleSocialPanel; }; }, []); // Fetch user on mount useEffect(() => { const fetchUser = async () => { try { const res = await fetch('/me', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setUser(data); } } catch (e) { console.error('Failed to fetch user:', e); } }; fetchUser(); }, []); // Fetch battles const fetchBattles = useCallback(async () => { try { const res = await fetch('/barbarian/list', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setBattles(data || []); } } catch (e) { console.error('Failed to fetch battles:', e); } finally { setIsLoading(false); } }, []); // Fetch history const fetchHistory = useCallback(async () => { try { const res = await fetch('/barbarian/history', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setHistory(data || []); } } catch (e) { console.error('Failed to fetch history:', e); } }, []); useEffect(() => { fetchBattles(); fetchHistory(); }, [fetchBattles, fetchHistory]); // WebSocket connection useEffect(() => { const connect = () => { const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${protocol}://${window.location.host}/ws/lobby`); ws.onopen = () => { console.log('๐Ÿ”— WebSocket connected'); setIsConnected(true); }; ws.onclose = () => { console.log('๐Ÿ”Œ WebSocket disconnected'); setIsConnected(false); setTimeout(connect, CONFIG.wsReconnectDelay); }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); handleWSMessage(msg); } catch (e) { console.error('WS parse error:', e); } }; wsRef.current = ws; }; connect(); return () => { if (wsRef.current) { wsRef.current.close(); } }; }, []); // Handle WebSocket messages const handleWSMessage = useCallback((msg) => { console.log('๐Ÿ“จ WS Message:', msg.type, msg); switch (msg.type) { case 'bb_create': // New battle created fetchBattles(); break; case 'bb_joined': // Someone joined a battle - just update the list // The countdown will be handled by bb_countdown message fetchBattles(); // If it's our battle, set up the battle data (countdown will come from bb_countdown) if (userRef.current && (msg.creator === userRef.current.username || msg.opponent === userRef.current.username)) { const battle = { id: msg.id, creator: msg.creator, opponent: msg.opponent, amount: msg.amount * 100, // Convert to cents join_amount: (msg.join_amount || msg.amount) * 100, total_pot: (msg.amount + (msg.join_amount || msg.amount)) * 100, status: 'active', creator_hp: 100, opponent_hp: 100, current_turn: msg.first_turn, first_turn: msg.first_turn }; setActiveBattle(battle); setBattleLog([]); setBattlePhase('waiting'); // Wait for bb_countdown console.log('๐ŸŽฒ bb_joined - Battle set up, waiting for countdown:', battle.id); } break; case 'bb_countdown': { // Countdown update from server - BOTH players see this console.log('โฑ๏ธ bb_countdown received:', msg); console.log('โฑ๏ธ userRef.current:', userRef.current); console.log('โฑ๏ธ activeBattleRef.current:', activeBattleRef.current); // If we're involved in this battle (either as creator or joiner) const myUsername = userRef.current?.username; const isCreatorMatch = msg.creator === myUsername; const isOpponentMatch = msg.opponent === myUsername; const isBattleMatch = activeBattleRef.current?.id === msg.battle_id; console.log('โฑ๏ธ Checking involvement:', { myUsername, msgCreator: msg.creator, msgOpponent: msg.opponent, isCreatorMatch, isOpponentMatch, isBattleMatch }); const isInvolvedInBattle = isBattleMatch || isCreatorMatch || isOpponentMatch; if (isInvolvedInBattle) { console.log('โœ… User IS involved in battle, showing countdown!'); // If we don't have the battle open yet (joiner case), set it up if (!activeBattleRef.current || activeBattleRef.current.id !== msg.battle_id) { const battle = { id: msg.battle_id, creator: msg.creator, opponent: msg.opponent, status: 'active', creator_hp: 100, opponent_hp: 100, current_turn: msg.first_turn, first_turn: msg.first_turn }; setActiveBattle(battle); setBattleLog([]); } // Set first attacker info setDiceRoll({ rolling: false, result: msg.first_turn, firstPlayer: msg.first_attacker }); setBattlePhase('countdown'); const countdownSeconds = msg.countdown_seconds || 5; setCountdown(countdownSeconds); setBattleLog([{ message: `${msg.first_attacker} attacks first!`, type: 'system' }]); // Determine if it's our turn const isCreator = msg.creator === userRef.current?.username; const myRole = isCreator ? 'creator' : 'opponent'; const nowMyTurn = msg.first_turn === myRole; setIsMyTurn(nowMyTurn); isMyTurnRef.current = nowMyTurn; console.log('โฑ๏ธ bb_countdown - Turn setup:', { isCreator, myRole, firstTurn: msg.first_turn, nowMyTurn }); // Start countdown timer synced with server start_time const startTime = msg.start_time ? new Date(msg.start_time).getTime() : Date.now() + (countdownSeconds * 1000); const updateCountdown = () => { const remaining = Math.max(0, Math.ceil((startTime - Date.now()) / 1000)); setCountdown(remaining); if (remaining > 0) { setTimeout(updateCountdown, 100); } }; updateCountdown(); } else { console.log('โŒ User NOT involved in this battle, ignoring countdown'); } break; } case 'bb_start': // Battle started - countdown finished, time to fight! console.log('๐Ÿš€ bb_start received:', msg); console.log('๐Ÿ” activeBattleRef.current:', activeBattleRef.current); console.log('๐Ÿ” msg.battle.id:', msg.battle?.id, 'activeBattleRef.current?.id:', activeBattleRef.current?.id); fetchBattles(); if (msg.battle && activeBattleRef.current?.id === msg.battle.id) { console.log('โœ… bb_start matches - starting fight phase'); // Convert amounts from coins to cents for display consistency const battleData = { ...msg.battle, amount: (msg.battle.amount || 0) * 100, join_amount: (msg.battle.join_amount || 0) * 100, total_pot: (msg.battle.total_pot || 0) * 100, }; setActiveBattle(prev => ({ ...prev, ...battleData, creator_hp: msg.hp?.creator ?? 100, opponent_hp: msg.hp?.opponent ?? 100, current_turn: msg.current_turn_role })); setCountdown(0); setDiceRoll(null); // Clear any remaining dice roll state setBattlePhase('fighting'); // Determine if it's our turn if (userRef.current) { const battleCreator = msg.battle.creator; const myUsername = userRef.current.username; const isCreator = battleCreator === myUsername; const myRole = isCreator ? 'creator' : 'opponent'; const currentTurn = msg.current_turn_role || msg.battle.current_turn; const nowMyTurn = currentTurn === myRole; console.log('๐ŸŽฎ bb_start - Turn calculation:', { battleCreator, myUsername, isCreator, myRole, currentTurn, 'msg.current_turn_role': msg.current_turn_role, 'msg.battle.current_turn': msg.battle.current_turn, nowMyTurn }); setIsMyTurn(nowMyTurn); isMyTurnRef.current = nowMyTurn; // Keep ref in sync immediately // Sync turn timer with server deadline const deadline = msg.turn_deadline ? new Date(msg.turn_deadline).getTime() : Date.now() + 15000; if (turnTimerRef.current) clearInterval(turnTimerRef.current); const updateTurnTimer = () => { const remaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000)); setTurnTimer(remaining); if (remaining > 0) { turnTimerRef.current = setTimeout(updateTurnTimer, 500); } }; updateTurnTimer(); } else { console.warn('โš ๏ธ userRef.current is null in bb_start'); } const currentPlayer = msg.current_player || (msg.current_turn_role === 'creator' ? msg.battle.creator : msg.battle.opponent); setBattleLog(prev => [...prev, { message: `[FIGHT] ${currentPlayer}'s turn!`, type: 'system' }]); } else { console.log('โš ๏ธ bb_start - battle ID mismatch or no msg.battle'); } break; case 'bb_move': console.log('๐Ÿ“ฅ Received bb_move:', msg); // Attack happened if (msg.battle_id === activeBattleRef.current?.id) { console.log('โœ… bb_move matches active battle'); // Determine who was the target (opposite of attacker) const attackerRole = msg.attacker_role; const targetRole = attackerRole === 'creator' ? 'opponent' : 'creator'; const moveType = msg.move || msg.move_type || 'normal'; // quick, normal, hard const isPotion = msg.is_potion || moveType === 'potion'; console.log('๐ŸŽฌ Animation details:', { attackerRole, targetRole, moveType, isPotion, damage: msg.damage, hit: msg.hit }); if (isPotion) { // Potion: Show drinking animation on attacker, then heal effect console.log('๐Ÿงช Setting potion animation'); setAttackAnimation({ attacker: attackerRole, type: 'potion' // This triggers sprite-potion CSS class }); // After drinking animation, show heal effect setTimeout(() => { setAttackAnimation(null); console.log('๐Ÿ’š Setting heal effect'); setDamageEffect({ target: attackerRole, // Heal targets self damage: msg.heal || 0, hit: true, isHeal: true }); // Clear heal effect after animation setTimeout(() => { setDamageEffect(null); }, 1200); }, 600); } else { // Regular attack: Trigger attack animation on attacker FIRST console.log('โš”๏ธ Setting attack animation:', { attacker: attackerRole, type: moveType }); setAttackAnimation({ attacker: attackerRole, type: moveType }); // After a short delay, show damage on target setTimeout(() => { setAttackAnimation(null); // Trigger damage effect animation console.log('๐Ÿ’ฅ Setting damage effect:', { target: targetRole, damage: msg.damage, hit: msg.hit }); setDamageEffect({ target: targetRole, damage: msg.damage || 0, hit: msg.hit, isHeal: false }); // Clear damage effect after animation completes setTimeout(() => { setDamageEffect(null); }, 1200); }, 400); } setActiveBattle(prev => prev ? { ...prev, creator_hp: msg.hp?.creator ?? prev.creator_hp, opponent_hp: msg.hp?.opponent ?? prev.opponent_hp, current_turn: msg.next_turn_role || msg.next_turn, creator_potion_used: msg.potion_used?.creator ?? prev.creator_potion_used, opponent_potion_used: msg.potion_used?.opponent ?? prev.opponent_potion_used } : prev); // Add to battle log const attacker = msg.attacker || 'Someone'; const moveLabel = msg.move_label || msg.move_type || 'attack'; const logMessage = msg.is_potion ? `[HEAL] ${attacker} uses Health Potion for +${msg.heal} HP!` : msg.hit ? `[HIT] ${attacker} uses ${moveLabel} for ${msg.damage} damage!` : `[MISS] ${attacker} uses ${moveLabel} - missed!`; setBattleLog(prev => [...prev, { message: logMessage, type: msg.is_potion ? 'heal' : (msg.hit ? 'hit' : 'miss') }]); // Check if it's our turn now and sync timer with server deadline if (userRef.current) { const isCreator = activeBattleRef.current?.creator === userRef.current.username; const myRole = isCreator ? 'creator' : 'opponent'; const nextTurnRole = msg.next_turn_role || msg.next_turn; const nowMyTurn = nextTurnRole === myRole; setIsMyTurn(nowMyTurn); isMyTurnRef.current = nowMyTurn; // Keep ref in sync immediately console.log('๐Ÿ”„ bb_move - Turn updated:', { myRole, nextTurnRole, nowMyTurn }); // Sync turn timer with server deadline if (turnTimerRef.current) { clearTimeout(turnTimerRef.current); clearInterval(turnTimerRef.current); } const deadline = msg.turn_deadline ? new Date(msg.turn_deadline).getTime() : Date.now() + 15000; const updateTurnTimer = () => { const remaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000)); setTurnTimer(remaining); if (remaining > 0) { turnTimerRef.current = setTimeout(updateTurnTimer, 500); } }; updateTurnTimer(); } } else { console.log('โš ๏ธ bb_move for different battle:', { msgBattleId: msg.battle_id, activeBattleId: activeBattleRef.current?.id }); } break; case 'bb_move_ack': console.log('โœ… Move acknowledged by server:', msg); break; case 'bb_move_error': console.error('โŒ Move error from server:', msg); // Re-enable turn if there was an error setIsMyTurn(true); isMyTurnRef.current = true; alert(`Attack failed: ${msg.error || 'Unknown error'}`); break; case 'bb_resolved': // Battle ended fetchBattles(); fetchHistory(); // Refresh history from server // Add to history ticker (convert from coins to cents for display) const totalPotCents = typeof msg.total_pot === 'number' ? msg.total_pot * 100 : (activeBattleRef.current?.total_pot || 0); setHistory(prev => [{ id: msg.battle_id, creator: activeBattleRef.current?.creator || msg.creator, opponent: activeBattleRef.current?.opponent || msg.opponent, winner: msg.winner, total_pot: totalPotCents, amount: activeBattleRef.current?.amount || 0 }, ...prev].slice(0, 50)); if (msg.battle_id === activeBattleRef.current?.id || msg.id === activeBattleRef.current?.id) { // Clear turn timer if (turnTimerRef.current) { clearInterval(turnTimerRef.current); turnTimerRef.current = null; } setActiveBattle(prev => prev ? { ...prev, status: 'resolved', winner: msg.winner, creator_hp: msg.hp?.creator ?? prev.creator_hp, opponent_hp: msg.hp?.opponent ?? prev.opponent_hp, winner_prize: msg.prize } : prev); const winnerName = msg.winner || 'Someone'; setBattleLog(prev => [...prev, { message: `[VICTORY] ${winnerName} WINS THE BATTLE!`, type: 'victory' }]); setIsMyTurn(false); setTurnTimer(0); setBattlePhase('finished'); // Show pending balance animation now that battle is finished if (window.showPendingBalanceAnimation) { setTimeout(() => window.showPendingBalanceAnimation(), 500); } } break; case 'bb_cancelled': // Battle was cancelled (expired or manually) console.log('Battle cancelled:', msg); fetchBattles(); // If we're viewing this battle, close it if (activeBattleRef.current?.id === msg.battle_id) { setActiveBattle(null); setBattlePhase('waiting'); } break; case 'balance_update': if (userRef.current && msg.username === userRef.current.username) { const oldBalance = userRef.current.balance || 0; const newBalance = msg.balance; const delta = newBalance - oldBalance; setUser(prev => prev ? { ...prev, balance: newBalance } : prev); // Queue animation if battle in progress, show immediately if finished if (delta > 0 && window.updateCoinRushBalance) { const isBattling = ['dice_roll', 'countdown', 'fighting'].includes(battlePhaseRef.current); window.updateCoinRushBalance(newBalance, delta, !isBattling); } } break; } }, [fetchBattles, fetchHistory]); // Keep activeBattle ref in sync const activeBattleRef = useRef(null); useEffect(() => { activeBattleRef.current = activeBattle; }, [activeBattle]); // Create duel const handleCreateDuel = async (stake) => { if (!user || isCreating) return; setIsCreating(true); try { const res = await fetch('/barbarian/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ amount: stake * 100 }) }); if (res.ok) { const battle = await res.json(); setBattles(prev => [battle, ...prev]); const newBalance = (user.balance || 0) - stake * 100; setUser(prev => prev ? { ...prev, balance: newBalance } : prev); // Show floating -bet animation if (window.updateCoinRushBalance) { window.updateCoinRushBalance(newBalance, -stake * 100); } } else { const error = await res.json(); alert(error.detail || 'Failed to create duel'); } } catch (e) { console.error('Create duel error:', e); alert('Failed to create duel'); } finally { setIsCreating(false); } }; // Join duel const handleJoinDuel = async (battleId) => { if (!user) { window.openAuthModal?.('login'); return; } // Prevent double-clicks if (isJoining) { console.log('Already joining, ignoring duplicate click'); return; } setIsJoining(true); console.log('Attempting to join battle:', battleId); console.log('๐Ÿ”Œ WebSocket connected:', isConnected); console.log('๐Ÿ‘ค Current user:', userRef.current); try { const res = await fetch('/barbarian/join', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ battle_id: battleId }) }); console.log('Join response status:', res.status, res.ok); if (res.ok) { const battle = await res.json(); console.log('Join successful, battle:', battle); // Set the battle data and phase - start countdown immediately for joiner if (!activeBattleRef.current || activeBattleRef.current.id !== battle.id) { setActiveBattle(battle); setBattleLog([]); // For joiner: start countdown immediately since backend is starting it const firstAttacker = battle.current_turn === 'creator' ? battle.creator : battle.opponent; setBattlePhase('countdown'); setDiceRoll({ rolling: false, result: battle.current_turn, firstPlayer: firstAttacker }); setCountdown(5); // Backend uses 5 second countdown setBattleLog([{ message: `${firstAttacker} attacks first!`, type: 'system' }]); // Set turn info const isCreator = battle.creator === userRef.current?.username; const myRole = isCreator ? 'creator' : 'opponent'; const nowMyTurn = battle.current_turn === myRole; setIsMyTurn(nowMyTurn); isMyTurnRef.current = nowMyTurn; console.log('๐Ÿ“ฑ HTTP Join - Battle set up with countdown!', { battle: battle.id, firstAttacker, nowMyTurn }); // Start local countdown timer (bb_countdown will sync with server time if received) const startTime = Date.now() + 5000; const updateCountdown = () => { const remaining = Math.max(0, Math.ceil((startTime - Date.now()) / 1000)); setCountdown(remaining); if (remaining > 0) { setTimeout(updateCountdown, 100); } }; setTimeout(updateCountdown, 100); } // Refresh user balance try { const userRes = await fetch('/me', { credentials: 'include' }); if (userRes.ok) { setUser(await userRes.json()); } } catch (balanceErr) { console.error('Failed to refresh balance:', balanceErr); } fetchBattles(); } else { // Only show error if we're not already in a battle (WebSocket might have beaten us) if (!activeBattleRef.current) { let errorMsg = 'Failed to join duel'; try { const error = await res.json(); errorMsg = error.detail || errorMsg; } catch (parseErr) { console.error('Failed to parse error response:', parseErr); } console.error('Join failed with status:', res.status, errorMsg); alert(errorMsg); } else { console.log('HTTP failed but WebSocket already opened battle, ignoring error'); } } } catch (e) { // Only show error if we're not already in a battle if (!activeBattleRef.current) { console.error('Join duel error:', e); alert('Failed to join duel: ' + e.message); } else { console.log('HTTP error but WebSocket already opened battle, ignoring'); } } finally { setIsJoining(false); } }; // View/spectate battle (also used for Resume Battle) const handleViewBattle = async (battleId) => { try { const res = await fetch(`/barbarian/battle/${battleId}`, { credentials: 'include' }); if (res.ok) { const battle = await res.json(); setActiveBattle(battle); // Set battle phase based on status if (battle.status === 'open') { // Spectating an open battle - show waiting phase setBattlePhase('waiting'); setCountdown(0); setBattleLog([{ message: 'Spectating - waiting for opponent...', type: 'system' }]); } else if (battle.status === 'active') { setBattlePhase('fighting'); setCountdown(0); setDiceRoll(null); setBattleLog([{ message: 'Battle in progress...', type: 'system' }]); // Check if participant and set turn state if (user) { const isCreator = battle.creator === user.username; const myRole = isCreator ? 'creator' : 'opponent'; const currentTurnRole = battle.current_turn; const isMyTurnNow = currentTurnRole === myRole; setIsMyTurn(isMyTurnNow); // Sync turn timer with server deadline if available if (turnTimerRef.current) { clearTimeout(turnTimerRef.current); clearInterval(turnTimerRef.current); } const deadline = battle.turn_deadline ? new Date(battle.turn_deadline).getTime() : Date.now() + 15000; const updateTurnTimer = () => { const remaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000)); setTurnTimer(remaining); if (remaining > 0) { turnTimerRef.current = setTimeout(updateTurnTimer, 500); } }; updateTurnTimer(); } } else if (battle.status === 'resolved') { setBattlePhase('finished'); setIsMyTurn(false); setBattleLog([{ message: 'Battle finished', type: 'system' }]); } } } catch (e) { console.error('View battle error:', e); } }; // Make attack via WebSocket const handleAttack = (attackType) => { const currentBattle = activeBattleRef.current; const myTurn = isMyTurnRef.current; console.log('๐Ÿ—ก๏ธ handleAttack called:', { attackType, battleId: currentBattle?.id, isMyTurn: myTurn, battlePhase: battlePhaseRef.current, wsState: wsRef.current?.readyState }); if (!currentBattle) { console.warn('โŒ Attack blocked: No active battle'); return; } if (!myTurn) { console.warn('โŒ Attack blocked: Not my turn (isMyTurnRef.current =', myTurn, ')'); return; } // Send attack via WebSocket if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { const message = { type: 'battle_move', battle_id: currentBattle.id, move_type: attackType }; console.log('๐Ÿ“ค Sending battle_move:', message); wsRef.current.send(JSON.stringify(message)); setIsMyTurn(false); // Optimistically disable isMyTurnRef.current = false; // Also update ref immediately setTurnTimer(0); } else { console.error('โŒ WebSocket not connected - readyState:', wsRef.current?.readyState); } }; // Keyboard shortcuts for attacks useEffect(() => { const handleKeyDown = (e) => { if (!activeBattle || !isMyTurn) return; const isCreator = user?.username === activeBattle.creator; const myPotionUsed = isCreator ? activeBattle.creator_potion_used : activeBattle.opponent_potion_used; switch (e.key) { case '1': handleAttack('quick'); break; case '2': handleAttack('normal'); break; case '3': handleAttack('hard'); break; case '4': if (!myPotionUsed) handleAttack('potion'); break; } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [activeBattle, isMyTurn]); // Countdown effect useEffect(() => { if (countdown <= 0) return; const timer = setInterval(() => { setCountdown(prev => { if (prev <= 1) { clearInterval(timer); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(timer); }, [countdown]); // Logout handler const handleLogout = async () => { try { await fetch('/auth/logout', { method: 'POST', credentials: 'include' }); setUser(null); window.location.href = '/'; } catch (e) {} }; // Handle auth success const handleAuthSuccess = async () => { setShowAuthModal(false); try { const res = await fetch('/me', { credentials: 'include' }); if (res.ok) { const data = await res.json(); setUser(data); } } catch (e) {} }; return (
setShowRules(false)} /> setShowDailyModal(false)} user={user} onClaim={() => { // Refresh user data after claiming fetch('/me', { credentials: 'include' }) .then(res => res.ok ? res.json() : null) .then(data => { if (data) setUser(data); }) .catch(() => {}); }} /> {/* Social Panel */} {user && showSocialPanel && ( setShowSocialPanel(false)} currentUser={user} /> )} {activeBattle && ( { // Don't clear activeBattle - just hide the overlay // The battle continues in the background setActiveBattle(null); }} onAttack={handleAttack} isMyTurn={isMyTurn} battleLog={battleLog} countdown={countdown} turnTimer={turnTimer} diceRoll={diceRoll} battlePhase={battlePhase} damageEffect={damageEffect} attackAnimation={attackAnimation} /> )} {/* History Ticker */}
{history.length > 0 ? ( <> {history.map((b, i) => (
#{b.id} {b.winner} defeated {b.winner === b.creator ? b.opponent : b.creator} for {formatCoins(b.total_pot || b.amount * 2)} coins
))} {/* Duplicate for seamless loop */} {history.map((b, i) => (
#{b.id} {b.winner} defeated {b.winner === b.creator ? b.opponent : b.creator} for {formatCoins(b.total_pot || b.amount * 2)} coins
))} ) : ( /* Placeholder when no history */
No recent battles yet...
)}
{/* Left Column - Create Duel */} {/* Center - Live Duels */}
{/* Right Column - Chat (Full Height) */}
); } // ============================================ // RENDER // ============================================ const root = ReactDOM.createRoot(document.getElementById('root')); root.render();