Merge pull request #199 from KoalaTrapLord/fix/deck-loading-and-backend-detection
fix: Deck loading — wire setupDeckChoices, fetch .dck content, add precon/deck endpoint
This commit is contained in:
3 files changed
+89
-13
No files matched your search
+45
-4
@@ -10877,16 +10877,20 @@ function parseDeckList(text) {
|
|||||||
let line = raw.trim();
|
let line = raw.trim();
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
|
|
||||||
// Section headers
|
// Section headers — supports "// Commander", "Commander:", "[Commander]", "[Main]", "[metadata]"
|
||||||
if (/^\/\/\s*/i.test(line) || /^(COMMANDER|COMPANION|SIDEBOARD|MAYBEBOARD|MAINBOARD|DECK|LAND)[:\s]*$/i.test(line)) {
|
if (/^\/\/\s*/i.test(line) || /^\[?(COMMANDER|COMPANION|SIDEBOARD|MAYBEBOARD|MAINBOARD|MAIN|DECK|LAND|METADATA)\]?[:\s]*$/i.test(line)) {
|
||||||
const cleaned = line.replace(/^\/\/\s*/, '').replace(/:$/, '').trim().toLowerCase();
|
const cleaned = line.replace(/^\/\/\s*/, '').replace(/[\[\]:]/g, '').trim().toLowerCase();
|
||||||
if (/commander/i.test(cleaned)) currentSection = 'commander';
|
if (/commander/i.test(cleaned)) currentSection = 'commander';
|
||||||
else if (/companion/i.test(cleaned)) currentSection = 'companion';
|
else if (/companion/i.test(cleaned)) currentSection = 'companion';
|
||||||
else if (/sideboard/i.test(cleaned)) currentSection = 'sideboard';
|
else if (/sideboard/i.test(cleaned)) currentSection = 'sideboard';
|
||||||
|
else if (/metadata/i.test(cleaned)) currentSection = 'metadata';
|
||||||
else currentSection = 'mainboard';
|
else currentSection = 'mainboard';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip metadata lines like "Name=Elven Empire"
|
||||||
|
if (currentSection === 'metadata') continue;
|
||||||
|
|
||||||
// Skip comment lines
|
// Skip comment lines
|
||||||
if (/^#/.test(line) || /^\*/.test(line)) continue;
|
if (/^#/.test(line) || /^\*/.test(line)) continue;
|
||||||
|
|
||||||
@@ -12719,7 +12723,44 @@ async function loadPreconForPlayer(playerIdx, preconId) {
|
|||||||
var player = gameState.players[playerIdx];
|
var player = gameState.players[playerIdx];
|
||||||
if (!player) return;
|
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;
|
if (!parsedCards.length) return;
|
||||||
|
|
||||||
var allCardNames = [];
|
var allCardNames = [];
|
||||||
|
|||||||
+19
-9
@@ -1123,7 +1123,23 @@ function initSetupScreen() {
|
|||||||
// Sync AI config to engine globals
|
// Sync AI config to engine globals
|
||||||
setupAiPlayers = _localAiSet;
|
setupAiPlayers = _localAiSet;
|
||||||
setupAiDifficulty = _localAiDiff;
|
setupAiDifficulty = _localAiDiff;
|
||||||
|
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);
|
initGame(selectedPlayerCount, playerNames, startingLife);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trigger display update for player count
|
// Trigger display update for player count
|
||||||
@@ -1461,15 +1477,9 @@ function initGame(playerCount, playerNames, startingLife) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign precon decks if selected
|
// NOTE: Precon deck loading is handled by the "Begin Battle" button
|
||||||
for (let i = 0; i < playerCount; i++) {
|
// which calls startGameWithDecks() when setupDeckChoices has entries.
|
||||||
const sel = document.getElementById('deck-sel-' + i);
|
// The old deck-sel-{i} dropdown path is removed.
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark AI players
|
// Mark AI players
|
||||||
setupAiPlayers.forEach(function(idx) {
|
setupAiPlayers.forEach(function(idx) {
|
||||||
|
|||||||
@@ -29,6 +29,31 @@ async def list_precons():
|
|||||||
return {"precons": PRECON_INDEX} # reads current value every call
|
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")
|
@router.post("/api/lab/precons/install")
|
||||||
async def install_precon(req: dict):
|
async def install_precon(req: dict):
|
||||||
"""Install a precon deck to the Forge decks directory.
|
"""Install a precon deck to the Forge decks directory.
|
||||||
|
|||||||
Reference in new issue
Block a user