feat: Implement solo draft mode with bot players and automated deck building.
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.c9el36ma12"
|
||||
"revision": "0.5drsp6r8gnc"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@@ -305,9 +305,9 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
const handleStartSoloTest = async () => {
|
||||
if (packs.length === 0) return;
|
||||
|
||||
// Validate Lands
|
||||
// Validate Lands - Warn but allow proceed (server will handle it or deck builder will be landless)
|
||||
if (!availableLands || availableLands.length === 0) {
|
||||
if (!confirm("No basic lands detected in the current pool. The generated deck will have 0 lands. Continue?")) {
|
||||
if (!confirm("No basic lands detected in the current pool. Decks might be invalid. Continue?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -315,49 +315,18 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Collect all cards
|
||||
const allCards = packs.flatMap(p => p.cards);
|
||||
|
||||
// Random Deck Construction Logic
|
||||
// 1. Separate lands and non-lands (Exclude existing Basic Lands from spells to be safe)
|
||||
const spells = allCards.filter(c => !c.typeLine?.includes('Basic Land') && !c.typeLine?.includes('Land'));
|
||||
|
||||
// 2. Select 23 Spells randomly
|
||||
const deckSpells: any[] = [];
|
||||
const spellPool = [...spells];
|
||||
|
||||
// Fisher-Yates Shuffle
|
||||
for (let i = spellPool.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[spellPool[i], spellPool[j]] = [spellPool[j], spellPool[i]];
|
||||
}
|
||||
|
||||
// Take up to 23 spells, or all if fewer
|
||||
deckSpells.push(...spellPool.slice(0, Math.min(23, spellPool.length)));
|
||||
|
||||
// 3. Select 17 Lands (or fill to 40)
|
||||
const deckLands: any[] = [];
|
||||
const landCount = 40 - deckSpells.length; // Aim for 40 cards total
|
||||
|
||||
if (availableLands.length > 0) {
|
||||
for (let i = 0; i < landCount; i++) {
|
||||
const land = availableLands[Math.floor(Math.random() * availableLands.length)];
|
||||
deckLands.push(land);
|
||||
}
|
||||
}
|
||||
|
||||
const fullDeck = [...deckSpells, ...deckLands];
|
||||
|
||||
// Emit socket event
|
||||
const playerId = localStorage.getItem('player_id') || 'tester-' + Date.now();
|
||||
const playerName = localStorage.getItem('player_name') || 'Tester';
|
||||
|
||||
if (!socketService.socket.connected) socketService.connect();
|
||||
|
||||
// Emit new start_solo_test event
|
||||
// Now sends PACKS and LANDS instead of a constructed DECK
|
||||
const response = await socketService.emitPromise('start_solo_test', {
|
||||
playerId,
|
||||
playerName,
|
||||
deck: fullDeck
|
||||
packs,
|
||||
basicLands: availableLands
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
@@ -369,7 +338,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
onGoToLobby();
|
||||
}, 100);
|
||||
} else {
|
||||
alert("Failed to start test game: " + response.message);
|
||||
alert("Failed to start solo draft: " + response.message);
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
@@ -793,10 +762,10 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
onClick={handleReset}
|
||||
disabled={loading}
|
||||
className={`w-full mt-4 py-2.5 px-4 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-2 ${loading
|
||||
? 'opacity-50 cursor-not-allowed text-slate-600 border border-transparent'
|
||||
: confirmClear
|
||||
? 'bg-red-600 text-white border border-red-500 shadow-md animate-pulse'
|
||||
: 'text-red-400 border border-red-900/30 hover:bg-red-950/30 hover:border-red-500/50 hover:text-red-300 shadow-sm'
|
||||
? 'opacity-50 cursor-not-allowed text-slate-600 border border-transparent'
|
||||
: confirmClear
|
||||
? 'bg-red-600 text-white border border-red-500 shadow-md animate-pulse'
|
||||
: 'text-red-400 border border-red-900/30 hover:bg-red-950/30 hover:border-red-500/50 hover:text-red-300 shadow-sm'
|
||||
}`}
|
||||
title="Clear all data and start over"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { socketService } from '../../services/SocketService';
|
||||
import { Users, MessageSquare, Send, Copy, Check, Layers, LogOut, Bell, BellOff, X } from 'lucide-react';
|
||||
import { Users, MessageSquare, Send, Copy, Check, Layers, LogOut, Bell, BellOff, X, Bot } from 'lucide-react';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import { useToast } from '../../components/Toast';
|
||||
import { GameView } from '../game/GameView';
|
||||
@@ -14,6 +14,7 @@ interface Player {
|
||||
isHost: boolean;
|
||||
role: 'player' | 'spectator';
|
||||
isOffline?: boolean;
|
||||
isBot?: boolean;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
@@ -283,7 +284,13 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
>
|
||||
<Layers className="w-5 h-5" /> Start Draft
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => socketService.socket.emit('add_bot', { roomId: room.id })}
|
||||
disabled={room.status !== 'waiting' || room.players.length >= 8}
|
||||
className="px-8 py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-lg flex items-center gap-2 shadow-lg shadow-indigo-900/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Bot className="w-5 h-5" /> Add Bot
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -426,8 +433,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
return (
|
||||
<div key={p.id} className="flex items-center justify-between bg-slate-900/80 p-3 rounded-xl border border-slate-700/50 hover:border-slate-600 transition-colors group">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm shadow-inner ${p.role === 'spectator' ? 'bg-slate-800 text-slate-500' : 'bg-gradient-to-br from-purple-600 to-blue-600 text-white shadow-purple-900/30'}`}>
|
||||
{p.name.substring(0, 2).toUpperCase()}
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm shadow-inner ${p.isBot ? 'bg-indigo-900 text-indigo-200 border border-indigo-500' : p.role === 'spectator' ? 'bg-slate-800 text-slate-500' : 'bg-gradient-to-br from-purple-600 to-blue-600 text-white shadow-purple-900/30'}`}>
|
||||
{p.isBot ? <Bot className="w-5 h-5" /> : p.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className={`text-sm font-bold ${isMe ? 'text-white' : 'text-slate-200'}`}>
|
||||
@@ -436,6 +443,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-500 flex items-center gap-1">
|
||||
{p.role}
|
||||
{p.isHost && <span className="text-amber-500 flex items-center">• Host</span>}
|
||||
{p.isBot && <span className="text-indigo-400 flex items-center">• Bot</span>}
|
||||
{isReady && room.status === 'deck_building' && <span className="text-emerald-500 flex items-center">• Ready</span>}
|
||||
{p.isOffline && <span className="text-red-500 flex items-center">• Offline</span>}
|
||||
</span>
|
||||
@@ -456,6 +464,17 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
<LogOut className="w-4 h-4 rotate-180" />
|
||||
</button>
|
||||
)}
|
||||
{isMeHost && p.isBot && (
|
||||
<button
|
||||
onClick={() => {
|
||||
socketService.socket.emit('remove_bot', { roomId: room.id, botId: p.id });
|
||||
}}
|
||||
className="p-1.5 hover:bg-red-500/10 rounded-lg text-slate-500 hover:text-red-500 transition-colors"
|
||||
title="Remove Bot"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{isMe && (
|
||||
<button onClick={onExit} className="p-1.5 hover:bg-red-500/10 rounded-lg text-slate-400 hover:text-red-400 transition-colors" title="Accions">
|
||||
<LogOut className="w-4 h-4" />
|
||||
|
||||
Reference in New Issue
Block a user