diff --git a/lan-client/engine.js b/lan-client/engine.js index fce6293..0995b57 100644 --- a/lan-client/engine.js +++ b/lan-client/engine.js @@ -10877,16 +10877,20 @@ function parseDeckList(text) { let line = raw.trim(); if (!line) continue; - // Section headers - if (/^\/\/\s*/i.test(line) || /^(COMMANDER|COMPANION|SIDEBOARD|MAYBEBOARD|MAINBOARD|DECK|LAND)[:\s]*$/i.test(line)) { - const cleaned = line.replace(/^\/\/\s*/, '').replace(/:$/, '').trim().toLowerCase(); + // Section headers — supports "// Commander", "Commander:", "[Commander]", "[Main]", "[metadata]" + if (/^\/\/\s*/i.test(line) || /^\[?(COMMANDER|COMPANION|SIDEBOARD|MAYBEBOARD|MAINBOARD|MAIN|DECK|LAND|METADATA)\]?[:\s]*$/i.test(line)) { + const cleaned = line.replace(/^\/\/\s*/, '').replace(/[\[\]:]/g, '').trim().toLowerCase(); if (/commander/i.test(cleaned)) currentSection = 'commander'; else if (/companion/i.test(cleaned)) currentSection = 'companion'; else if (/sideboard/i.test(cleaned)) currentSection = 'sideboard'; + else if (/metadata/i.test(cleaned)) currentSection = 'metadata'; else currentSection = 'mainboard'; continue; } + // Skip metadata lines like "Name=Elven Empire" + if (currentSection === 'metadata') continue; + // Skip comment lines if (/^#/.test(line) || /^\*/.test(line)) continue; @@ -12719,7 +12723,44 @@ async function loadPreconForPlayer(playerIdx, preconId) { var player = gameState.players[playerIdx]; if (!player) return; - var parsedCards = parseDeckList(precon.decklist); + // Try to get decklist text: embedded first, then fetch from backend .dck file + var decklistText = precon.decklist || ''; + if (!decklistText && precon.fileName) { + // Fetch .dck file from backend if available + if (typeof AIBridge !== 'undefined' && AIBridge.connector.connected) { + try { + var deckData = await AIBridge.deckSource.fetchPreconDeck(precon.fileName); + if (deckData && deckData.decklist) { + decklistText = deckData.decklist; + } else if (deckData && typeof deckData === 'string') { + decklistText = deckData; + } + } catch (e) { + console.warn('[loadPreconForPlayer] Backend deck fetch failed:', e); + } + } + // Fallback: try to fetch .dck file directly from precon-decks/ (same origin) + if (!decklistText) { + try { + var resp = await fetch('precon-decks/' + precon.fileName); + if (resp.ok) { + decklistText = await resp.text(); + } + } catch (e) { + // Fallback: try relative to backend + try { + var resp2 = await fetch('/precon-decks/' + precon.fileName); + if (resp2.ok) { + decklistText = await resp2.text(); + } + } catch (e2) { + console.warn('[loadPreconForPlayer] Could not fetch .dck file:', precon.fileName); + } + } + } + } + + var parsedCards = parseDeckList(decklistText); if (!parsedCards.length) return; var allCardNames = []; diff --git a/lan-client/ui.js b/lan-client/ui.js index 2caeaca..4cf7ab2 100644 --- a/lan-client/ui.js +++ b/lan-client/ui.js @@ -1123,7 +1123,23 @@ function initSetupScreen() { // Sync AI config to engine globals setupAiPlayers = _localAiSet; setupAiDifficulty = _localAiDiff; - initGame(selectedPlayerCount, playerNames, startingLife); + setupPlayerCount = selectedPlayerCount; + setupStartingLife = startingLife; + + // Sync player names to engine globals + for (let i = 0; i < playerNames.length; i++) { + var nameInput = document.getElementById('player-name-' + i); + if (nameInput) nameInput.value = playerNames[i]; + } + + // If any deck choices were made, use startGameWithDecks (async deck loading) + var hasDecks = typeof setupDeckChoices !== 'undefined' && + Object.keys(setupDeckChoices).length > 0; + if (hasDecks && typeof startGameWithDecks === 'function') { + startGameWithDecks(); + } else { + initGame(selectedPlayerCount, playerNames, startingLife); + } }; // Trigger display update for player count @@ -1461,15 +1477,9 @@ function initGame(playerCount, playerNames, startingLife) { } } - // Assign precon decks if selected - for (let i = 0; i < playerCount; i++) { - const sel = document.getElementById('deck-sel-' + i); - if (sel && sel.value) { - const preconList2 = (typeof PRECON_DATA !== 'undefined') ? PRECON_DATA : (typeof PRECONS !== 'undefined') ? PRECONS : []; - const precon = preconList2.find(p => p.id === sel.value); - if (precon) loadPreconDeck(i, precon); - } - } + // NOTE: Precon deck loading is handled by the "Begin Battle" button + // which calls startGameWithDecks() when setupDeckChoices has entries. + // The old deck-sel-{i} dropdown path is removed. // Mark AI players setupAiPlayers.forEach(function(idx) { diff --git a/routes/precon.py b/routes/precon.py index 593d443..f17ab7f 100644 --- a/routes/precon.py +++ b/routes/precon.py @@ -29,6 +29,31 @@ async def list_precons(): return {"precons": PRECON_INDEX} # reads current value every call +@router.get("/api/lab/precons/deck") +async def get_precon_deck(fileName: str): + """Return the raw decklist content of a precon .dck file. + Query: ?fileName=Elven_Empire.dck + """ + if not fileName: + raise HTTPException(400, "fileName query parameter is required") + # Sanitize: only allow .dck files, no path traversal + if '..' in fileName or '/' in fileName or '\\' in fileName: + raise HTTPException(400, "Invalid fileName") + if not fileName.endswith('.dck'): + raise HTTPException(400, "Only .dck files are supported") + + src = _get_precon_dir() / fileName + if not src.exists(): + raise HTTPException(404, f"Precon not found: {fileName}") + + try: + content = src.read_text(encoding='utf-8') + except OSError as e: + raise HTTPException(500, f"Failed to read deck file: {e}") + + return {"fileName": fileName, "decklist": content} + + @router.post("/api/lab/precons/install") async def install_precon(req: dict): """Install a precon deck to the Forge decks directory.