Compare commits

..
Author SHA1 Message Date
Commander AI Lab 73f286b369 fix: setup screen overflows viewport — constrain height and use 2-col grid
- Add max-height: 92vh and overflow-y: auto to .setup-card so it scrolls
  instead of extending past the screen
- Widen .setup-card to 860px to accommodate 2-column deck grid
- Change .deck-assignment-grid from vertical stack to 2-column grid so
  4 player panels fit in 2 rows instead of 4
- Reduce .deck-tab-content max-height to 170px and .deck-precon-list to
  140px for tighter panels
- Make gradient bar sticky so it stays visible when scrolling
- Add responsive fallback to single column below 640px
2026-04-03 13:36:27 +00:00
Commander AI Lab edcd6f4c52 fix: add precons.js with 163 entries and fix backend precon UI refresh
- Generate lan-client/precons.js from precon-decks/precon-index.json so
  PRECON_DATA is always available for local/offline mode (163 precons)
- Add scripts/generate-precons-js.js for regeneration when precons update
- Fix _loadBackendPrecons() to re-render the precon tab UI when backend
  precons load successfully (was only updating status indicator text,
  not populating the dropdown)

Without precons.js, PRECON_DATA was undefined and the dropdown showed
'No precon data loaded' even though 163 .dck files exist in the repo.
2026-04-03 13:30:53 +00:00
3 changed files with 14 additions and 90 deletions

No files matched your search

+4 -45
View File
@@ -10877,20 +10877,16 @@ function parseDeckList(text) {
let line = raw.trim();
if (!line) continue;
// 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();
// 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();
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;
@@ -12723,44 +12719,7 @@ async function loadPreconForPlayer(playerIdx, preconId) {
var player = gameState.players[playerIdx];
if (!player) return;
// 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);
var parsedCards = parseDeckList(precon.decklist);
if (!parsedCards.length) return;
var allCardNames = [];
+10 -20
View File
@@ -1123,23 +1123,7 @@ function initSetupScreen() {
// Sync AI config to engine globals
setupAiPlayers = _localAiSet;
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
@@ -1477,9 +1461,15 @@ function initGame(playerCount, playerNames, startingLife) {
}
}
// 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.
// 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);
}
}
// Mark AI players
setupAiPlayers.forEach(function(idx) {
-25
View File
@@ -29,31 +29,6 @@ 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.