fix: refine LAN battlefield integration with full JSDoc, proper phase IDs
- Add comprehensive JSDoc comments to all ai-bridge.js classes - Use optional chaining for PHASES lookup in GameStateBridge - Direct addLogEntry calls in player-panels.js (loaded after engine.js) - Remove leftover mana pool CSS that was missed in initial pass - Minor style.css cleanup for consistency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
36742d3cd5
commit
2a6e3ac893
5 files changed
+148
-48
No files matched your search
+99
-21
@@ -1,6 +1,5 @@
|
||||
// ai-bridge.js — Commander-AI-Lab Backend Integration Bridge
|
||||
// Mediates between the LAN Battlefield App and the Commander-AI-Lab backend.
|
||||
// Contains: BackendConnector, GameStateBridge, MacroActionMapper, DeckSourceClient
|
||||
// ai-bridge.js — Commander-AI-Lab Backend Bridge
|
||||
// Manages REST + WebSocket connectivity to the Commander-AI-Lab backend.
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -52,6 +51,10 @@ class BackendConnector {
|
||||
|
||||
// ── Connection Management ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Test connectivity to the backend. Returns true if the backend
|
||||
* is reachable and the health endpoint responds.
|
||||
*/
|
||||
async checkHealth() {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
@@ -82,6 +85,9 @@ class BackendConnector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ML policy model is loaded and ready for inference.
|
||||
*/
|
||||
async checkModelStatus() {
|
||||
if (!this._connected) return false;
|
||||
try {
|
||||
@@ -100,6 +106,10 @@ class BackendConnector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full initialization: health check + model status.
|
||||
* Returns { connected, modelLoaded }.
|
||||
*/
|
||||
async initialize() {
|
||||
const connected = await this.checkHealth();
|
||||
let modelLoaded = false;
|
||||
@@ -112,6 +122,10 @@ class BackendConnector {
|
||||
|
||||
// ── REST API Calls ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST to any backend endpoint with JSON body.
|
||||
* Includes retry logic and timeout handling.
|
||||
*/
|
||||
async _post(path, body, timeoutMs) {
|
||||
const url = `${this.config.backendUrl}${path}`;
|
||||
let lastError = null;
|
||||
@@ -148,6 +162,9 @@ class BackendConnector {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET from any backend endpoint.
|
||||
*/
|
||||
async _get(path, timeoutMs) {
|
||||
const url = `${this.config.backendUrl}${path}`;
|
||||
const controller = new AbortController();
|
||||
@@ -168,6 +185,10 @@ class BackendConnector {
|
||||
|
||||
// ── WebSocket Management ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Open a WebSocket connection for a game session.
|
||||
* Receives state deltas, decision events, and game-over signals.
|
||||
*/
|
||||
connectWebSocket(gameId, clientId) {
|
||||
if (!this.config.enableWebSocket) return;
|
||||
if (this._ws) this.disconnectWebSocket();
|
||||
@@ -322,6 +343,10 @@ class GameStateBridge {
|
||||
|
||||
// ── Session Lifecycle ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a game session on the backend when a new game starts.
|
||||
* Maps the LAN app's player setup to SeatConfig objects.
|
||||
*/
|
||||
async initSession(gameState) {
|
||||
if (!this.connector.connected) return null;
|
||||
|
||||
@@ -353,6 +378,9 @@ class GameStateBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map LAN app difficulty to backend AI profile.
|
||||
*/
|
||||
_mapAiProfile(difficulty) {
|
||||
const map = {
|
||||
easy: 'AI',
|
||||
@@ -364,6 +392,10 @@ class GameStateBridge {
|
||||
|
||||
// ── Game State Serialization ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Convert the JS gameState to a DecisionSnapshot-compatible
|
||||
* object for the /api/policy/decide endpoint.
|
||||
*/
|
||||
serializeGameState(gameState, playerIdx) {
|
||||
const players = gameState.players.map((p, i) => {
|
||||
// Count creatures on battlefield
|
||||
@@ -434,9 +466,7 @@ class GameStateBridge {
|
||||
});
|
||||
|
||||
// Map current phase to backend format
|
||||
const phaseId = typeof PHASES !== 'undefined' && PHASES[gameState.currentPhaseIndex]
|
||||
? PHASES[gameState.currentPhaseIndex].id
|
||||
: 'main1';
|
||||
const phaseId = PHASES[gameState.currentPhaseIndex]?.id || 'main1';
|
||||
const phaseMap = {
|
||||
beginning: 'main_1',
|
||||
main1: 'main_1',
|
||||
@@ -460,6 +490,9 @@ class GameStateBridge {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all battlefield cards for a player.
|
||||
*/
|
||||
_getBattlefieldCards(player, gameState) {
|
||||
if (!gameState || !gameState.battlefield) return [];
|
||||
return (gameState.battlefield || []).filter(
|
||||
@@ -467,6 +500,9 @@ class GameStateBridge {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer a playstyle hint for the AI based on the deck composition.
|
||||
*/
|
||||
_inferPlaystyle(gameState, playerIdx) {
|
||||
const player = gameState.players[playerIdx];
|
||||
if (!player) return 'midrange';
|
||||
@@ -493,6 +529,10 @@ class GameStateBridge {
|
||||
|
||||
// ── AI Decision Request ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Request an AI decision from the backend policy server.
|
||||
* Returns the macro-action prediction or null if unavailable.
|
||||
*/
|
||||
async requestAiDecision(gameState, playerIdx) {
|
||||
if (!this.connector.connected || !this.connector.modelLoaded) {
|
||||
return null;
|
||||
@@ -520,6 +560,9 @@ class GameStateBridge {
|
||||
|
||||
// ── State Sync ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Push current game state to the backend WebSocket layer.
|
||||
*/
|
||||
async pushState(gameState) {
|
||||
if (!this._sessionGameId || !this.connector.connected) return;
|
||||
if (!AI_BRIDGE_CONFIG.enableStateSync) return;
|
||||
@@ -534,12 +577,16 @@ class GameStateBridge {
|
||||
snapshot
|
||||
);
|
||||
} catch (e) {
|
||||
// Non-critical — log but don't fail
|
||||
if (AI_BRIDGE_CONFIG.verbose) {
|
||||
console.warn('[GameStateBridge] State push failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push an AI decision event to connected WebSocket clients.
|
||||
*/
|
||||
async pushDecision(decision) {
|
||||
if (!this._sessionGameId || !this.connector.connected) return;
|
||||
|
||||
@@ -586,6 +633,15 @@ class GameStateBridge {
|
||||
// ============================================================
|
||||
|
||||
class MacroActionMapper {
|
||||
/**
|
||||
* Execute a macro-action by calling the appropriate engine.js functions.
|
||||
*
|
||||
* @param {string} action - One of the 8 MacroAction values
|
||||
* @param {number} playerIdx - Index of the AI player
|
||||
* @param {object} gameState - Current game state
|
||||
* @param {object} decision - Full decision response from backend
|
||||
* @returns {boolean} Whether the action was successfully executed
|
||||
*/
|
||||
static execute(action, playerIdx, gameState, decision) {
|
||||
const player = gameState.players[playerIdx];
|
||||
if (!player) return false;
|
||||
@@ -595,22 +651,32 @@ class MacroActionMapper {
|
||||
switch (action) {
|
||||
case 'cast_creature':
|
||||
return MacroActionMapper._castCreature(playerIdx, player, gameState, confidence);
|
||||
|
||||
case 'cast_removal':
|
||||
return MacroActionMapper._castRemoval(playerIdx, player, gameState);
|
||||
|
||||
case 'cast_draw':
|
||||
return MacroActionMapper._castDraw(playerIdx, player, gameState);
|
||||
|
||||
case 'cast_ramp':
|
||||
return MacroActionMapper._castRamp(playerIdx, player, gameState);
|
||||
|
||||
case 'cast_commander':
|
||||
return MacroActionMapper._castCommander(playerIdx, player, gameState);
|
||||
|
||||
case 'attack_opponent':
|
||||
return MacroActionMapper._attackOpponent(playerIdx, player, gameState);
|
||||
|
||||
case 'hold_mana':
|
||||
MacroActionMapper._log(playerIdx, player, 'holding mana open', confidence);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
'holding mana open', confidence);
|
||||
return true;
|
||||
|
||||
case 'pass':
|
||||
MacroActionMapper._log(playerIdx, player, 'passing', confidence);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
'passing', confidence);
|
||||
return true;
|
||||
|
||||
default:
|
||||
console.warn('[MacroActionMapper] Unknown action:', action);
|
||||
return false;
|
||||
@@ -633,7 +699,7 @@ class MacroActionMapper {
|
||||
if (MacroActionMapper._canAfford(playerIdx, card)) {
|
||||
const idx = hand.indexOf(card);
|
||||
if (idx !== -1) {
|
||||
if (!card.imageUrl && typeof generateAiCardSVG === 'function') card.imageUrl = generateAiCardSVG(card);
|
||||
if (!card.imageUrl) card.imageUrl = generateAiCardSVG(card);
|
||||
playCardFromHand(playerIdx, idx);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
`cast <strong>${card.name}</strong> (creature slot)`, confidence);
|
||||
@@ -654,7 +720,7 @@ class MacroActionMapper {
|
||||
if (MacroActionMapper._canAfford(playerIdx, card)) {
|
||||
const idx = hand.indexOf(card);
|
||||
if (idx !== -1) {
|
||||
if (!card.imageUrl && typeof generateAiCardSVG === 'function') card.imageUrl = generateAiCardSVG(card);
|
||||
if (!card.imageUrl) card.imageUrl = generateAiCardSVG(card);
|
||||
playCardFromHand(playerIdx, idx);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
`cast <strong>${card.name}</strong>`, confidence);
|
||||
@@ -678,7 +744,7 @@ class MacroActionMapper {
|
||||
if (MacroActionMapper._canAfford(playerIdx, card)) {
|
||||
const idx = hand.indexOf(card);
|
||||
if (idx !== -1) {
|
||||
if (!card.imageUrl && typeof generateAiCardSVG === 'function') card.imageUrl = generateAiCardSVG(card);
|
||||
if (!card.imageUrl) card.imageUrl = generateAiCardSVG(card);
|
||||
playCardFromHand(playerIdx, idx);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
`cast removal <strong>${card.name}</strong>`);
|
||||
@@ -707,7 +773,7 @@ class MacroActionMapper {
|
||||
if (MacroActionMapper._canAfford(playerIdx, card)) {
|
||||
const idx = hand.indexOf(card);
|
||||
if (idx !== -1) {
|
||||
if (!card.imageUrl && typeof generateAiCardSVG === 'function') card.imageUrl = generateAiCardSVG(card);
|
||||
if (!card.imageUrl) card.imageUrl = generateAiCardSVG(card);
|
||||
playCardFromHand(playerIdx, idx);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
`cast draw spell <strong>${card.name}</strong>`);
|
||||
@@ -728,7 +794,7 @@ class MacroActionMapper {
|
||||
const landIdx = hand.findIndex(c => MacroActionMapper._isLand(c));
|
||||
if (landIdx !== -1) {
|
||||
const land = hand[landIdx];
|
||||
if (!land.imageUrl && typeof generateAiCardSVG === 'function') land.imageUrl = generateAiCardSVG(land);
|
||||
if (!land.imageUrl) land.imageUrl = generateAiCardSVG(land);
|
||||
playCardFromHand(playerIdx, landIdx);
|
||||
player.aiLandPlayedThisTurn = true;
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
@@ -737,7 +803,7 @@ class MacroActionMapper {
|
||||
}
|
||||
}
|
||||
|
||||
// Then try ramp spells
|
||||
// Then try ramp spells (mana rocks, rampant growth effects)
|
||||
const rampSpells = hand.filter(c =>
|
||||
MacroActionMapper._looksLikeRamp(c)
|
||||
);
|
||||
@@ -752,7 +818,7 @@ class MacroActionMapper {
|
||||
if (MacroActionMapper._canAfford(playerIdx, card)) {
|
||||
const idx = hand.indexOf(card);
|
||||
if (idx !== -1) {
|
||||
if (!card.imageUrl && typeof generateAiCardSVG === 'function') card.imageUrl = generateAiCardSVG(card);
|
||||
if (!card.imageUrl) card.imageUrl = generateAiCardSVG(card);
|
||||
playCardFromHand(playerIdx, idx);
|
||||
MacroActionMapper._log(playerIdx, player,
|
||||
`cast ramp <strong>${card.name}</strong>`);
|
||||
@@ -808,7 +874,7 @@ class MacroActionMapper {
|
||||
}
|
||||
|
||||
static _canAfford(playerIdx, card) {
|
||||
if (typeof gameState === 'undefined' || !gameState) return false;
|
||||
if (!gameState) return false;
|
||||
const available = typeof aiCountUntappedLands === 'function'
|
||||
? aiCountUntappedLands(playerIdx)
|
||||
: 0;
|
||||
@@ -850,11 +916,9 @@ class MacroActionMapper {
|
||||
const confStr = confidence
|
||||
? ` <span style="color:var(--color-text-faint)">(${Math.round(confidence * 100)}% conf.)</span>`
|
||||
: '';
|
||||
if (typeof addLogEntry === 'function') {
|
||||
addLogEntry(
|
||||
`\u{1F9E0} <strong>${player.name}</strong> ${message}${confStr} [ML]`
|
||||
);
|
||||
}
|
||||
addLogEntry(
|
||||
`\u{1F9E0} <strong>${player.name}</strong> ${message}${confStr} [ML]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,9 +934,14 @@ class DeckSourceClient {
|
||||
this._cacheTtlMs = 5 * 60 * 1000; // 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch precon decks from the backend.
|
||||
* Returns array: [{ fileName, deckName, commander, colors, set, ... }]
|
||||
*/
|
||||
async fetchPrecons(forceRefresh = false) {
|
||||
if (!this.connector.connected) return [];
|
||||
|
||||
// Check cache
|
||||
if (!forceRefresh && this._preconCache &&
|
||||
(Date.now() - this._preconCacheTime) < this._cacheTtlMs) {
|
||||
return this._preconCache;
|
||||
@@ -889,6 +958,9 @@ class DeckSourceClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch EDHREC recommendations for a commander.
|
||||
*/
|
||||
async fetchEdhrecDecks(commanderName) {
|
||||
if (!this.connector.connected) return [];
|
||||
try {
|
||||
@@ -901,6 +973,9 @@ class DeckSourceClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deck content (card list) for a specific precon.
|
||||
*/
|
||||
async fetchPreconDeck(fileName) {
|
||||
if (!this.connector.connected) return null;
|
||||
try {
|
||||
@@ -931,6 +1006,9 @@ window.AIBridge = {
|
||||
deckSource: aiBridgeDeckSource,
|
||||
MacroActionMapper: MacroActionMapper,
|
||||
|
||||
/**
|
||||
* Convenience: initialize the bridge and report status.
|
||||
*/
|
||||
async init() {
|
||||
const status = await aiBridgeConnector.initialize();
|
||||
if (status.connected) {
|
||||
|
||||
+35
-7
@@ -248,6 +248,9 @@ function createInitialState(playerCount, playerNames, startingLife) {
|
||||
startingLife,
|
||||
poison: 0,
|
||||
energy: 0,
|
||||
experience: 0,
|
||||
rad: 0,
|
||||
emblems: [],
|
||||
eliminated: false,
|
||||
commanderDamage: {}, // keyed by opponent player id
|
||||
zones: {
|
||||
@@ -262,9 +265,6 @@ function createInitialState(playerCount, playerNames, startingLife) {
|
||||
commanderCardName: '',
|
||||
commanderCastCount2: 0,
|
||||
commanderCardName2: '',
|
||||
experience: 0,
|
||||
rad: 0,
|
||||
emblems: [],
|
||||
};
|
||||
// Initialize commander damage counters
|
||||
for (let j = 0; j < playerCount; j++) {
|
||||
@@ -5144,8 +5144,6 @@ function createPlayerPanel(player, isActive) {
|
||||
<button class="life-btn" onclick="adjustLife(${player.id}, 1)" oncontextmenu="event.preventDefault();adjustLife(${player.id}, 5)" title="Left: +1 | Right: +5">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Mana pool display removed -->
|
||||
|
||||
<div class="cmd-damage-section">
|
||||
<div class="cmd-damage-label">
|
||||
<span>CMD Damage</span>
|
||||
@@ -5244,8 +5242,6 @@ function adjustLife(playerId, amount) {
|
||||
}
|
||||
|
||||
setLife(playerId, player.life + amount);
|
||||
// Sync enhanced player panels
|
||||
if (typeof renderEnhancedPanels === 'function') renderEnhancedPanels();
|
||||
}
|
||||
|
||||
function setLife(playerId, newLife) {
|
||||
@@ -5294,6 +5290,8 @@ function setLife(playerId, newLife) {
|
||||
eliminatePlayer(playerId, 'life total reached 0');
|
||||
}
|
||||
checkStateBasedActions();
|
||||
// Sync enhanced player panels
|
||||
if (typeof renderEnhancedPanels === 'function') renderEnhancedPanels();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -12300,6 +12298,36 @@ function toggleManaAutoClear() {
|
||||
|
||||
function renderManaPool() {
|
||||
// Mana pool display removed — internal tracking only.
|
||||
// No visual update needed.
|
||||
}
|
||||
|
||||
// Legacy renderManaPool body preserved for reference (removed from UI)
|
||||
function _renderManaPoolLegacy() {
|
||||
if (!gameState) return;
|
||||
const pid = getSelectedManaPlayerId();
|
||||
const player = gameState.players[pid];
|
||||
if (!player) return;
|
||||
|
||||
let total = 0;
|
||||
MANA_COLORS.forEach(c => {
|
||||
const val = player.manaPool[c] || 0;
|
||||
total += val;
|
||||
const el = document.getElementById(`mana-${c}`);
|
||||
if (el) {
|
||||
el.textContent = val;
|
||||
el.classList.toggle('has-mana', val > 0);
|
||||
}
|
||||
});
|
||||
|
||||
const totalEl = document.getElementById('mana-total');
|
||||
if (totalEl) totalEl.textContent = total;
|
||||
|
||||
// Refresh hand castability badges when mana changes
|
||||
if (typeof renderHandArea === 'function') {
|
||||
// Debounce to avoid excessive rerenders
|
||||
if (renderManaPool._handTimer) clearTimeout(renderManaPool._handTimer);
|
||||
renderManaPool._handTimer = setTimeout(function() { renderHandArea(); }, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function populateManaPlayerSelect() {
|
||||
|
||||
+10
-13
@@ -5,6 +5,11 @@
|
||||
|
||||
/**
|
||||
* Create an enhanced player panel widget for a quadrant.
|
||||
* Replaces the basic life counter with a full tracking panel.
|
||||
*
|
||||
* @param {object} player - Player object from gameState
|
||||
* @param {number} quadrant - 1-4 (matches CSS class p1-p4)
|
||||
* @param {boolean} isActive - Whether this is the active player
|
||||
*/
|
||||
function createEnhancedPlayerPanel(player, quadrant, isActive) {
|
||||
var panel = document.createElement('div');
|
||||
@@ -178,9 +183,7 @@ function adjustEnergy(playerId, amount) {
|
||||
var player = gameState.players[playerId];
|
||||
if (!player) return;
|
||||
player.energy = Math.max(0, (player.energy || 0) + amount);
|
||||
if (typeof addLogEntry === 'function') {
|
||||
addLogEntry('<strong>' + player.name + '</strong> energy: ' + player.energy);
|
||||
}
|
||||
addLogEntry('<strong>' + player.name + '</strong> energy: ' + player.energy);
|
||||
renderEnhancedPanels();
|
||||
}
|
||||
|
||||
@@ -189,9 +192,7 @@ function adjustExperience(playerId, amount) {
|
||||
var player = gameState.players[playerId];
|
||||
if (!player) return;
|
||||
player.experience = Math.max(0, (player.experience || 0) + amount);
|
||||
if (typeof addLogEntry === 'function') {
|
||||
addLogEntry('<strong>' + player.name + '</strong> experience: ' + player.experience);
|
||||
}
|
||||
addLogEntry('<strong>' + player.name + '</strong> experience: ' + player.experience);
|
||||
renderEnhancedPanels();
|
||||
}
|
||||
|
||||
@@ -200,9 +201,7 @@ function adjustRad(playerId, amount) {
|
||||
var player = gameState.players[playerId];
|
||||
if (!player) return;
|
||||
player.rad = Math.max(0, (player.rad || 0) + amount);
|
||||
if (typeof addLogEntry === 'function') {
|
||||
addLogEntry('<strong>' + player.name + '</strong> rad counters: ' + player.rad);
|
||||
}
|
||||
addLogEntry('<strong>' + player.name + '</strong> rad counters: ' + player.rad);
|
||||
renderEnhancedPanels();
|
||||
}
|
||||
|
||||
@@ -214,9 +213,7 @@ function showAddEmblemDialog(playerId) {
|
||||
if (!player) return;
|
||||
if (!player.emblems) player.emblems = [];
|
||||
player.emblems.push({ name: name.trim() });
|
||||
if (typeof addLogEntry === 'function') {
|
||||
addLogEntry('<strong>' + player.name + '</strong> gained emblem: ' + name.trim());
|
||||
}
|
||||
addLogEntry('<strong>' + player.name + '</strong> gained emblem: ' + name.trim());
|
||||
renderEnhancedPanels();
|
||||
}
|
||||
|
||||
@@ -225,7 +222,7 @@ function removeEmblem(playerId, idx) {
|
||||
var player = gameState.players[playerId];
|
||||
if (!player || !player.emblems) return;
|
||||
var removed = player.emblems.splice(idx, 1);
|
||||
if (removed.length > 0 && typeof addLogEntry === 'function') {
|
||||
if (removed.length > 0) {
|
||||
addLogEntry('<strong>' + player.name + '</strong> lost emblem: ' + removed[0].name);
|
||||
}
|
||||
renderEnhancedPanels();
|
||||
|
||||
@@ -1460,8 +1460,6 @@ body.stack-pick-mode .battlefield-card {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Mana pool display removed — internal tracking only */
|
||||
|
||||
/* ============================================================
|
||||
TRIGGER STACK PANEL
|
||||
============================================================ */
|
||||
|
||||
+4
-5
@@ -1188,6 +1188,7 @@ function updateDeckAssignmentGrid(playerCount) {
|
||||
}
|
||||
|
||||
function _buildPreconTabContent(playerIdx) {
|
||||
// Local precons from precons.js (PRECON_DATA)
|
||||
var preconList = (typeof PRECON_DATA !== 'undefined') ? PRECON_DATA : [];
|
||||
|
||||
if (preconList.length === 0) {
|
||||
@@ -1297,11 +1298,9 @@ function filterPrecons(playerIdx, query) {
|
||||
var q = query.toLowerCase().trim();
|
||||
|
||||
list.querySelectorAll('.deck-precon-item').forEach(function(item) {
|
||||
var name = item.querySelector('.deck-precon-name');
|
||||
var cmdr = item.querySelector('.deck-precon-cmdr');
|
||||
var nameText = name ? name.textContent.toLowerCase() : '';
|
||||
var cmdrText = cmdr ? cmdr.textContent.toLowerCase() : '';
|
||||
var match = !q || nameText.includes(q) || cmdrText.includes(q);
|
||||
var name = item.querySelector('.deck-precon-name')?.textContent?.toLowerCase() || '';
|
||||
var cmdr = item.querySelector('.deck-precon-cmdr')?.textContent?.toLowerCase() || '';
|
||||
var match = !q || name.includes(q) || cmdr.includes(q);
|
||||
item.style.display = match ? '' : 'none';
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user