/**
* 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
STAKE (COINS)
setSoundEnabled(!soundEnabled)}
title={soundEnabled ? 'Mute sounds' : 'Enable sounds'}
>
{soundEnabled ? Icons.volumeOn : Icons.volumeOff}
setAmount(e.target.value)}
min={CONFIG.minStake}
/>
setAmount('')} className="quick-btn">Clear
adjustAmount(1)} className="quick-btn">+1
adjustAmount(5)} className="quick-btn">+5
adjustAmount(10)} className="quick-btn">+10
adjustAmount(50)} className="quick-btn">+50
adjustAmount(100)} className="quick-btn">+100
setAmount(Math.floor((parseInt(amount, 10) || 0) / 2).toString())} className="quick-btn">ยฝ
setAmount(((parseInt(amount, 10) || 0) * 2).toString())} className="quick-btn">ร2
Max
{isCreating ? (
Creating...
) : !user ? (
Login Required
) : (
<>
CREATE FIGHT
{Icons.sword}
>
)}
);
}
// ============================================
// 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 */}
onView(battle.id)} title={isResolved ? 'Watch replay' : 'Spectate'}>
{isResolved ? Icons.play : Icons.eye}
{/* Join button - for open battles that aren't your own */}
{battle.status === 'open' && !isOwn && (
onJoin(battle.id)}
disabled={isJoining || !user}
title={!user ? 'Login to join' : 'Join this duel'}
>
{isJoining ? '...' : <>{Icons.sword} Join>}
)}
{/* Resume button - for active battles you're in */}
{isActive && isParticipant && (
onView(battle.id)}>
Resume
)}
);
}
// ============================================
// 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
);
}
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 */}
{Icons.x}
{/* 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]) => (
{
console.log('๐ Attack button clicked:', key);
onAttack(key);
}}
>
{Icons[attack.icon]}
{attack.name}
{attack.hitChance}% Hit
{attack.damage[0]}-{attack.damage[1]} DMG
))}
{/* Potion Button - Side panel */}
{(() => {
const myPotionUsed = isCreator ? battle.creator_potion_used : battle.opponent_potion_used;
return (
{
console.log('๐งช Potion button clicked, used:', myPotionUsed);
if (!myPotionUsed) onAttack('potion');
}}
disabled={myPotionUsed}
title={myPotionUsed ? 'Already used this battle' : 'Use potion to heal 20-30 HP (doesn\'t end your turn)'}
>
{myPotionUsed ? 'Used' : `+${POTION.heal[0]}-${POTION.heal[1]} HP`}
);
})()}
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
)}
Close
)}
{/* 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()}>
{Icons.x}
{/* Header */}
{Icons.sword}
Knight Duel Rules
Master the art of combat and claim victory!
{/* Tab Navigation */}
setActiveTab('basics')}
>
{Icons.info} Basics
setActiveTab('combat')}
>
{Icons.sword} Combat
setActiveTab('items')}
>
{Icons.potion} Items
setActiveTab('rewards')}
>
{Icons.trophy} Rewards
{/* 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
+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 */}
Got it, let's fight!
);
}
// ============================================
// 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 */}
{Icons.close}
{/* Content */}
{loading ? (
{Icons.spinner}
Loading rewards...
) : !user ? (
{Icons.lock}
Login Required
Please login to claim your daily rewards!
{ onClose(); window.openAuthModal?.('login'); }}
>
Log In
) : (
<>
{/* 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
) : (
{claiming ? (
<>
{Icons.spinner}
Claiming...
>
) : (
<>
{Icons.gift}
Claim Reward
>
)}
)}
{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( );