+ +
+
+ +
+ + + +
+
+
- -
-
Basic Search
- -
-
- - -
-
- - -
-
- -
- - - - - -
-
-
- - @@ -546,61 +480,66 @@
- -
-
Item Stats
-
-
- - - - - -
-
- - - - - -
+ +
+
+ +
- -
-
- - - - - -
-
- - - - - -
+
+ +
- -
-
- - -
-
- - -
-
- - -
+
+ + +
+
+ + + - + +
+
+ + + - + +
+
+ + + - + +
+
+ + + - + +
+
+ + +
+
+ + +
+
+ +
- -
- -
-
Equipment Sets
-
+ +
+ +
@@ -666,11 +605,11 @@
-
+
- -
-
Legendary Cantrips
+ +
+
@@ -843,11 +782,11 @@
-
+
- -
-
Legendary Wards
+ +
+
@@ -882,12 +821,15 @@
-
+
- -
-
Equipment Slots
-
+ +
+ + + +
+
@@ -928,6 +870,11 @@
+
+ + +
+
@@ -945,7 +892,6 @@
-
diff --git a/static/inventory.js b/static/inventory.js index e77978fa..77469a81 100644 --- a/static/inventory.js +++ b/static/inventory.js @@ -255,10 +255,6 @@ function buildSearchParameters() { params.append('armor_only', 'true'); } else if (equipmentType === 'jewelry') { params.append('jewelry_only', 'true'); - } else if (equipmentType === 'shirt') { - params.append('shirt_only', 'true'); - } else if (equipmentType === 'pants') { - params.append('pants_only', 'true'); } // If 'all' is selected, don't add any type filter @@ -302,7 +298,6 @@ function buildSearchParameters() { addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost'); addParam(params, 'min_vitality_rating', 'searchMinVitalityRating'); addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating'); - addParam(params, 'min_crit_damage_resist_rating', 'searchMinCritDamageResistRating'); // Requirements parameters addParam(params, 'min_level', 'searchMinLevel'); @@ -443,7 +438,6 @@ function displayResults(data) { Heal Boost${getSortIcon('heal_boost_rating')} Vitality${getSortIcon('vitality_rating')} Dmg Resist${getSortIcon('damage_resist_rating')} - Crit Dmg Resist${getSortIcon('crit_damage_resist_rating')} Last Updated${getSortIcon('last_updated')} @@ -457,7 +451,6 @@ function displayResults(data) { const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-'; const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-'; const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-'; - const critDamageResistRating = item.crit_damage_resist_rating > 0 ? item.crit_damage_resist_rating : '-'; const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory'; const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory'; @@ -529,7 +522,6 @@ function displayResults(data) { ${healBoostRating} ${vitalityRating} ${damageResistRating} - ${critDamageResistRating} ${lastUpdated} `; diff --git a/static/suitbuilder.css b/static/suitbuilder.css index 3fe8a8d0..73d3577c 100644 --- a/static/suitbuilder.css +++ b/static/suitbuilder.css @@ -657,176 +657,6 @@ body { margin-left: 4px; } -/* Ratings display */ -.item-ratings { - color: #0066cc; - font-size: 11px; - font-weight: 600; - background: #e6f3ff; - padding: 1px 4px; - border-radius: 2px; - margin-left: 4px; -} - -/* Empty slot styling */ -.suit-item-entry.empty-slot { - opacity: 0.6; - background: #f8f9fa; - border-left: 3px solid #dee2e6; - padding-left: 8px; -} - -.empty-slot-text { - color: #6c757d; - font-style: italic; - font-size: 11px; -} - -/* New Column-Based Table Layout */ -.suit-items-table { - width: 100%; - font-size: 12px; - margin-top: 8px; -} - -.suit-items-header { - display: grid; - grid-template-columns: 80px 120px 250px 140px 250px 60px 120px; - gap: 8px; - background: #2c3e50; - color: white; - padding: 8px 4px; - font-weight: 600; - font-size: 11px; - border-radius: 4px 4px 0 0; -} - -.suit-items-header > div { - color: white !important; - opacity: 1 !important; -} - -.suit-items-body { - background: #f8f9fa; - border: 1px solid #dee2e6; - border-top: none; - border-radius: 0 0 4px 4px; -} - -.suit-item-row { - display: grid; - grid-template-columns: 80px 120px 250px 140px 250px 60px 120px; - gap: 8px; - padding: 6px 4px; - border-bottom: 1px solid #e9ecef; - align-items: center; - min-height: 24px; -} - -.suit-item-row:last-child { - border-bottom: none; -} - -.suit-item-row:nth-child(even) { - background: #ffffff; -} - -.suit-item-row.empty-slot { - opacity: 0.5; - color: #6c757d; - font-style: italic; -} - -/* Column styling */ -.col-slot { - font-weight: 600; - color: #495057; -} - -.col-character { - color: #666; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.col-item { - color: #333; - font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.col-set { - font-weight: 600; - font-size: 11px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.suit-items-body .col-set { - color: #1f2937; - background: #f3f4f6; - padding: 2px 4px; - border-radius: 3px; -} - -.col-spells { - color: #7c3aed; - font-size: 11px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.col-armor { - color: #059669; - font-weight: 600; - text-align: center; -} - -.col-ratings { - color: #0066cc; - font-size: 11px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.need-reducing { - color: #dc2626; - font-weight: bold; - margin-left: 2px; -} - -/* Responsive adjustments for table */ -@media (max-width: 1200px) { - .suit-items-header, - .suit-item-row { - grid-template-columns: 70px 100px 200px 120px 200px 50px 100px; - gap: 6px; - font-size: 11px; - } -} - -@media (max-width: 900px) { - .suit-items-header, - .suit-item-row { - grid-template-columns: 60px 80px 150px 100px 150px 40px 80px; - gap: 4px; - font-size: 10px; - } - - .col-spells, - .col-ratings { - font-size: 10px; - } -} - /* Progressive Search Styles */ .search-progress { margin-top: 15px; diff --git a/static/suitbuilder.js b/static/suitbuilder.js index 47a9a842..2c652c10 100644 --- a/static/suitbuilder.js +++ b/static/suitbuilder.js @@ -1,12 +1,10 @@ // Suitbuilder JavaScript - Constraint Solver Frontend Logic -console.log('Suitbuilder.js loaded - VERSION: SCORE_ORDERING_AND_CANCELLATION_FIX_v3'); // Configuration -const API_BASE = '/inv/suitbuilder'; +const API_BASE = '/inv'; let currentSuits = []; let lockedSlots = new Set(); let selectedSuit = null; -let currentSearchController = null; // AbortController for current search // Initialize when page loads document.addEventListener('DOMContentLoaded', function() { @@ -27,7 +25,7 @@ function initializeSuitbuilder() { */ async function loadCharacters() { try { - const response = await fetch(`${API_BASE}/characters`); + const response = await fetch(`${API_BASE}/characters/list`); if (!response.ok) { throw new Error('Failed to load characters'); } @@ -54,13 +52,11 @@ function displayCharacters(characters) { let html = ''; characters.forEach(character => { - // Sanitize character name for HTML ID (replace special chars with underscores) - const safeId = character.replace(/[^a-zA-Z0-9]/g, '_'); html += `
- - + +
`; }); @@ -165,14 +161,9 @@ async function performSuitSearch() { try { await streamOptimalSuits(constraints); } catch (error) { - // Don't show error for user-cancelled searches - if (error.name === 'AbortError') { - console.log('Search cancelled by user'); - } else { - console.error('Suit search error:', error); - resultsDiv.innerHTML = `
❌ Suit search failed: ${error.message}
`; - countSpan.textContent = ''; - } + console.error('Suit search error:', error); + resultsDiv.innerHTML = `
❌ Suit search failed: ${error.message}
`; + countSpan.textContent = ''; } } @@ -243,208 +234,66 @@ function validateConstraints(constraints) { * Stream optimal suits using Server-Sent Events with progressive results */ async function streamOptimalSuits(constraints) { - // Prepare constraint data for POST request - const requestBody = { - characters: constraints.characters.length > 0 ? constraints.characters : [], - primary_set: constraints.primary_set ? parseInt(constraints.primary_set) : null, - secondary_set: constraints.secondary_set ? parseInt(constraints.secondary_set) : null, - required_spells: [ - ...constraints.legendary_cantrips, - ...constraints.protection_spells - ], - locked_items: {}, // TODO: implement locked items - include_equipped: constraints.include_equipped, - include_inventory: constraints.include_inventory, - min_armor: constraints.min_armor ? parseInt(constraints.min_armor) : null, - max_armor: constraints.max_armor ? parseInt(constraints.max_armor) : null, - min_crit_damage: constraints.min_crit_damage ? parseInt(constraints.min_crit_damage) : null, - max_crit_damage: constraints.max_crit_damage ? parseInt(constraints.max_crit_damage) : null, - min_damage_rating: constraints.min_damage_rating ? parseInt(constraints.min_damage_rating) : null, - max_damage_rating: constraints.max_damage_rating ? parseInt(constraints.max_damage_rating) : null, - max_results: 10, - search_timeout: 300 - }; + // Build request parameters for the streaming constraint solver + const params = new URLSearchParams(); - console.log('Starting suit search with constraints:', requestBody); - - // Cancel any existing search - if (currentSearchController) { - currentSearchController.abort(); + // Character selection + if (constraints.characters.length > 0) { + params.append('characters', constraints.characters.join(',')); + } else { + params.append('include_all_characters', 'true'); } - // Create new AbortController for this search - currentSearchController = new AbortController(); - - // Use fetch with streaming response instead of EventSource for POST support - const response = await fetch(`${API_BASE}/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - signal: currentSearchController.signal // Add abort signal - }); - - if (!response.ok) { - throw new Error(`Search failed: ${response.statusText}`); + // Equipment sets + if (constraints.primary_set) { + params.append('primary_set', constraints.primary_set); + } + if (constraints.secondary_set) { + params.append('secondary_set', constraints.secondary_set); } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); + // Legendary cantrips + if (constraints.legendary_cantrips.length > 0) { + params.append('legendary_cantrips', constraints.legendary_cantrips.join(',')); + } + + // Legendary wards + if (constraints.protection_spells.length > 0) { + params.append('legendary_wards', constraints.protection_spells.join(',')); + } + + // Rating constraints + if (constraints.min_armor) params.append('min_armor', constraints.min_armor); + if (constraints.max_armor) params.append('max_armor', constraints.max_armor); + if (constraints.min_crit_damage) params.append('min_crit_damage', constraints.min_crit_damage); + if (constraints.max_crit_damage) params.append('max_crit_damage', constraints.max_crit_damage); + if (constraints.min_damage_rating) params.append('min_damage_rating', constraints.min_damage_rating); + if (constraints.max_damage_rating) params.append('max_damage_rating', constraints.max_damage_rating); + + // Equipment status + params.append('include_equipped', constraints.include_equipped.toString()); + params.append('include_inventory', constraints.include_inventory.toString()); + + // Locked slots + if (lockedSlots.size > 0) { + params.append('locked_slots', Array.from(lockedSlots).join(',')); + } + + // Search depth (default to balanced) + params.append('search_depth', 'balanced'); + + const streamUrl = `${API_BASE}/optimize/suits/stream?${params.toString()}`; + console.log('Streaming suits with URL:', streamUrl); return new Promise((resolve, reject) => { + const eventSource = new EventSource(streamUrl); let searchStopped = false; - let buffer = ''; - - async function readStream() { - try { - while (true) { - const { done, value } = await reader.read(); - - if (done) { - resolve(); - break; - } - - if (searchStopped) { - await reader.cancel(); - resolve(); - break; - } - - // Process SSE data - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Keep incomplete line in buffer - - let currentEventType = null; - - for (const line of lines) { - if (line.startsWith('event: ')) { - currentEventType = line.substring(7).trim(); - continue; - } - - if (line.startsWith('data: ')) { - const data = line.substring(6); - - try { - const eventData = JSON.parse(data); - - // Handle different event types based on the current event type - if (currentEventType === 'suit') { - handleSuitEvent(eventData); - } else if (currentEventType === 'progress') { - handleProgressEvent(eventData); - } else if (currentEventType === 'complete') { - handleCompleteEvent(eventData); - resolve(); - return; - } else if (currentEventType === 'error') { - handleErrorEvent(eventData); - reject(new Error(eventData.message || 'Search error')); - return; - } - - // Reset event type after processing - currentEventType = null; - - } catch (parseError) { - console.warn('Failed to parse SSE data:', data, 'Event type:', currentEventType); - } - } - } - } - } catch (error) { - // Don't treat abort as an error - if (error.name === 'AbortError') { - console.log('Search was aborted by user'); - resolve(); - } else { - reject(error); - } - } - } - - readStream(); - - // Event handlers - function handleSuitEvent(data) { - try { - console.log('NEW handleSuitEvent called with data:', data); - - // Transform backend suit format to frontend format - const transformedSuit = transformSuitData(data); - console.log('Transformed suit:', transformedSuit); - - // Insert suit in score-ordered position (highest score first) - const insertIndex = insertSuitInScoreOrder(transformedSuit); - console.log('Insert index returned:', insertIndex); - - // Insert DOM element at the correct position instead of regenerating everything - insertSuitDOMAtPosition(transformedSuit, insertIndex); - console.log('DOM insertion complete'); - - // Update count - document.getElementById('foundCount').textContent = currentSuits.length; - document.getElementById('resultsCount').textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`; - - } catch (error) { - console.error('Error processing suit data:', error); - console.error('Stack trace:', error.stack); - } - } - - function handleProgressEvent(data) { - try { - document.getElementById('foundCount').textContent = data.found || currentSuits.length; - document.getElementById('checkedCount').textContent = data.evaluated || 0; - document.getElementById('elapsedTime').textContent = data.elapsed || '0.0'; - } catch (error) { - console.error('Error processing progress data:', error); - } - } - - function handleCompleteEvent(data) { - try { - // Hide loading indicator - const loadingDiv = document.querySelector('.loading'); - if (loadingDiv) { - loadingDiv.innerHTML = `✅ Search complete! Found ${data.suits_found} suits in ${data.duration}s.`; - } - - // Update final results count - const countSpan = document.getElementById('resultsCount'); - if (countSpan) { - countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`; - } - - } catch (error) { - console.error('Error processing completion data:', error); - } - } - - function handleErrorEvent(data) { - try { - const loadingDiv = document.querySelector('.loading'); - if (loadingDiv) { - loadingDiv.innerHTML = `❌ Search error: ${data.message}`; - } - } catch (error) { - console.error('Error processing error data:', error); - } - } // Add stop search functionality const stopButton = document.getElementById('stopSearch'); stopButton.addEventListener('click', () => { searchStopped = true; - - // Actually abort the HTTP request - if (currentSearchController) { - currentSearchController.abort(); - currentSearchController = null; - } + eventSource.close(); // Update UI to show search was stopped const loadingDiv = document.querySelector('.loading'); @@ -457,7 +306,118 @@ async function streamOptimalSuits(constraints) { if (countSpan) { countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''} (search stopped)`; } + + resolve(); }); + + // Handle individual suit results + eventSource.addEventListener('suit', (event) => { + try { + const suit = JSON.parse(event.data); + + // Transform backend suit format to frontend format + const transformedSuit = transformSuitData(suit); + currentSuits.push(transformedSuit); + + // Add suit to streaming results + addSuitToResults(transformedSuit, currentSuits.length - 1); + + // Update count + document.getElementById('foundCount').textContent = currentSuits.length; + document.getElementById('resultsCount').textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`; + + } catch (error) { + console.error('Error processing suit data:', error); + } + }); + + // Handle progress updates + eventSource.addEventListener('progress', (event) => { + try { + const progress = JSON.parse(event.data); + document.getElementById('foundCount').textContent = progress.found || currentSuits.length; + document.getElementById('checkedCount').textContent = progress.checked || 0; + document.getElementById('elapsedTime').textContent = progress.elapsed || '0.0'; + } catch (error) { + console.error('Error processing progress data:', error); + } + }); + + // Handle search completion + eventSource.addEventListener('complete', (event) => { + try { + const completion = JSON.parse(event.data); + + // Hide loading indicator + const loadingDiv = document.querySelector('.loading'); + if (loadingDiv) { + loadingDiv.innerHTML = `✅ Search complete! Found ${completion.total_found} suits in ${completion.total_time}s.`; + } + + // Update final results count + const countSpan = document.getElementById('resultsCount'); + if (countSpan) { + countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`; + } + + eventSource.close(); + resolve(); + + } catch (error) { + console.error('Error processing completion data:', error); + eventSource.close(); + resolve(); + } + }); + + // Handle timeout + eventSource.addEventListener('timeout', (event) => { + try { + const timeout = JSON.parse(event.data); + + // Update UI to show timeout + const loadingDiv = document.querySelector('.loading'); + if (loadingDiv) { + loadingDiv.innerHTML = `⏰ ${timeout.message}`; + } + + eventSource.close(); + resolve(); + + } catch (error) { + console.error('Error processing timeout data:', error); + eventSource.close(); + resolve(); + } + }); + + // Handle errors + eventSource.addEventListener('error', (event) => { + try { + const errorData = JSON.parse(event.data); + console.error('Stream error:', errorData.message); + + const loadingDiv = document.querySelector('.loading'); + if (loadingDiv) { + loadingDiv.innerHTML = `❌ Search error: ${errorData.message}`; + } + + } catch (error) { + console.error('Error parsing error data:', error); + } + + eventSource.close(); + reject(new Error('Stream error occurred')); + }); + + // Handle connection errors + eventSource.onerror = (event) => { + if (!searchStopped) { + console.error('EventSource error:', event); + eventSource.close(); + reject(new Error('Connection error during streaming')); + } + }; }); } @@ -498,96 +458,20 @@ function transformSuitData(suit) { } /** - * Insert a suit into the currentSuits array in score-ordered position (highest first) + * Add a single suit to the streaming results display */ -function insertSuitInScoreOrder(suit) { - console.log(`Inserting suit with score ${suit.score}. Current suits:`, currentSuits.map(s => s.score)); - - // Find the correct position to insert the suit (highest score first) - let insertIndex = 0; - for (let i = 0; i < currentSuits.length; i++) { - if (suit.score > currentSuits[i].score) { - insertIndex = i; - break; - } - insertIndex = i + 1; - } - - // Insert the suit at the correct position - currentSuits.splice(insertIndex, 0, suit); - - console.log(`Inserted at index ${insertIndex}. New order:`, currentSuits.map(s => s.score)); - return insertIndex; -} - -/** - * Regenerate the entire results display to maintain proper score ordering - */ -function regenerateResultsDisplay() { - console.log('Regenerating display with suits:', currentSuits.map(s => `Score: ${s.score}, ID: ${s.id}`)); - +function addSuitToResults(suit, index) { const streamingResults = document.getElementById('streamingResults'); if (!streamingResults) return; - // Clear existing results - streamingResults.innerHTML = ''; - - // Re-add all suits in their current (score-ordered) positions - currentSuits.forEach((suit, index) => { - const scoreClass = getScoreClass(suit.score); - const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸'; - - const suitHtml = ` -
-
-
- ${medal} Suit #${suit.id} (Score: ${suit.score}) -
-
-
- ${formatSuitStats(suit)} -
-
- ${formatSuitItems(suit.items)} - ${suit.missing && suit.missing.length > 0 ? `
Missing: ${suit.missing.join(', ')}
` : ''} - ${suit.notes && suit.notes.length > 0 ? `
${suit.notes.join(' • ')}
` : ''} -
-
- `; - - streamingResults.insertAdjacentHTML('beforeend', suitHtml); - }); - - // Re-add click handlers for all suits - document.querySelectorAll('.suit-item').forEach(item => { - item.addEventListener('click', function() { - const suitId = parseInt(this.dataset.suitId); - selectSuit(suitId); - }); - }); -} - -/** - * Insert a suit DOM element at the correct position and update all medal rankings - */ -function insertSuitDOMAtPosition(suit, insertIndex) { - console.log('insertSuitDOMAtPosition called with suit score:', suit.score, 'at index:', insertIndex); - - const streamingResults = document.getElementById('streamingResults'); - if (!streamingResults) { - console.error('streamingResults element not found!'); - return; - } - - console.log('Current DOM children count:', streamingResults.children.length); - - // Create the new suit HTML const scoreClass = getScoreClass(suit.score); + const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸'; + const suitHtml = `
- 🔸 Suit #${suit.id} (Score: ${suit.score}) + ${medal} Suit #${suit.id} (Score: ${suit.score})
@@ -601,45 +485,16 @@ function insertSuitDOMAtPosition(suit, insertIndex) {
`; - // Insert at the correct position - const existingSuits = streamingResults.children; - if (insertIndex >= existingSuits.length) { - // Insert at the end - streamingResults.insertAdjacentHTML('beforeend', suitHtml); - } else { - // Insert before the suit at insertIndex - existingSuits[insertIndex].insertAdjacentHTML('beforebegin', suitHtml); - } - - // Update all medal rankings after insertion - updateAllMedals(); + streamingResults.insertAdjacentHTML('beforeend', suitHtml); // Add click handler for the new suit - const newSuitElement = streamingResults.children[insertIndex]; + const newSuitElement = streamingResults.lastElementChild; newSuitElement.addEventListener('click', function() { const suitId = parseInt(this.dataset.suitId); selectSuit(suitId); }); } -/** - * Update medal rankings for all displayed suits - */ -function updateAllMedals() { - const streamingResults = document.getElementById('streamingResults'); - if (!streamingResults) return; - - Array.from(streamingResults.children).forEach((suitElement, index) => { - const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸'; - const scoreElement = suitElement.querySelector('.suit-score'); - if (scoreElement) { - const scoreText = scoreElement.textContent; - // Replace the existing medal with the new one - scoreElement.textContent = scoreText.replace(/^[🥇🥈🥉🔸]\s*/, medal + ' '); - } - }); -} - /** * Generate suit combinations from available items * This is a simplified algorithm - the full constraint solver will be more sophisticated @@ -757,13 +612,10 @@ function displaySuitResults(suits) { return; } - // Sort suits by score (highest first) before displaying - const sortedSuits = [...suits].sort((a, b) => b.score - a.score); - - countSpan.textContent = `Found ${sortedSuits.length} suit${sortedSuits.length !== 1 ? 's' : ''}`; + countSpan.textContent = `Found ${suits.length} suit${suits.length !== 1 ? 's' : ''}`; let html = ''; - sortedSuits.forEach((suit, index) => { + suits.forEach((suit, index) => { const scoreClass = getScoreClass(suit.score); const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸'; @@ -808,177 +660,33 @@ function getScoreClass(score) { } /** - * Format suit items for display - shows ALL armor slots even if empty + * Format suit items for display */ function formatSuitItems(items) { - console.log(`[DEBUG] formatSuitItems called with items:`, items); + let html = ''; - // Define all expected armor/equipment slots in logical order - const allSlots = [ - // Armor slots - 'Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands', - 'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet', - // Jewelry slots - 'Neck', 'Left Ring', 'Right Ring', 'Left Wrist', 'Right Wrist', 'Trinket', - // Clothing slots - 'Shirt', 'Pants' - ]; + if (!items || Object.keys(items).length === 0) { + return '
No items in this suit
'; + } - console.log(`[DEBUG] allSlots:`, allSlots); - - // Create table structure with header - let html = ` -
-
-
Slot
-
Character
-
Item
-
Set
-
Spells
-
Armor
-
Ratings
+ Object.entries(items).forEach(([slot, item]) => { + const needsReducing = isMultiSlotItem(item) ? 'Need Reducing' : ''; + const properties = formatItemProperties(item); + + html += ` +
+ ${slot}: + ${item.character_name} - + ${item.name} + ${properties ? `(${properties})` : ''} + ${needsReducing}
-
- `; - - allSlots.forEach(slot => { - const item = items ? items[slot] : null; - - // DEBUG: Log all slots and items - console.log(`[DEBUG] Processing slot '${slot}', item:`, item); - - if (item) { - // Item exists in this slot - const character = item.source_character || item.character_name || 'Unknown'; - const itemName = item.name || 'Unknown Item'; - - // Only show set names for armor items (not jewelry or clothing) - const armorSlots = ['Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands', - 'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet']; - const setName = (armorSlots.includes(slot) && item.set_name) ? item.set_name : '-'; - - const spells = formatItemSpells(item); - const armor = formatItemArmor(item); - const ratings = formatItemRatingsColumns(item); - const needsReducing = isMultiSlotItem(item) ? '*' : ''; - - html += ` -
-
${slot}
-
${character}
-
${itemName}${needsReducing}
-
${setName}
-
${spells}
-
${armor}
-
${ratings}
-
- `; - } else { - // Empty slot - html += ` -
-
${slot}
-
-
-
-
-
-
-
-
-
-
-
-
-
- `; - } + `; }); - html += ` -
-
- `; - return html; } -/** - * Format item spells for column display (focus on Legendary spells) - */ -function formatItemSpells(item) { - const spellArray = item.spells || item.spell_names || []; - if (!Array.isArray(spellArray) || spellArray.length === 0) { - return '-'; - } - - // Filter for important spells (Legendary, Epic) - const importantSpells = spellArray.filter(spell => - spell.includes('Legendary') || spell.includes('Epic') - ); - - if (importantSpells.length === 0) { - return `${spellArray.length} spells`; - } - - // Show up to 2 important spells, abbreviate the rest - const displaySpells = importantSpells.slice(0, 2); - let result = displaySpells.join(', '); - - if (importantSpells.length > 2) { - result += ` +${importantSpells.length - 2} more`; - } - - return result; -} - -/** - * Format item armor for column display - */ -function formatItemArmor(item) { - if (item.armor_level && item.armor_level > 0) { - return item.armor_level.toString(); - } - return '-'; -} - -/** - * Format item ratings for column display - */ -function formatItemRatingsColumns(item) { - const ratings = []; - - // Access ratings from the ratings object if available, fallback to direct properties - const itemRatings = item.ratings || item; - - // Helper function to get rating value, treating null/undefined/negative as 0 - function getRatingValue(value) { - if (value === null || value === undefined || value < 0) return 0; - return Math.round(value); // Round to nearest integer - } - - // Determine if this is clothing (shirts/pants) or armor - // Check item name patterns since ObjectClass 3 items (clothing) may appear in various slots - const itemName = item.name || ''; - const isClothing = itemName.toLowerCase().includes('shirt') || - itemName.toLowerCase().includes('pants') || - itemName.toLowerCase().includes('breeches') || - itemName.toLowerCase().includes('baggy') || - (item.slot === 'Shirt' || item.slot === 'Pants'); - - if (isClothing) { - // Clothing: Show DR and DRR - const damageRating = getRatingValue(itemRatings.damage_rating); - const damageResist = getRatingValue(itemRatings.damage_resist_rating); - - ratings.push(`DR${damageRating}`); - ratings.push(`DRR${damageResist}`); - } else { - // Armor: Show CD and CDR - const critDamage = getRatingValue(itemRatings.crit_damage_rating); - const critDamageResist = getRatingValue(itemRatings.crit_damage_resist_rating); - - ratings.push(`CD${critDamage}`); - ratings.push(`CDR${critDamageResist}`); - } - - return ratings.join(' '); -} - /** * Check if item is multi-slot and needs reducing * Only armor items need reduction - jewelry can naturally go in multiple slots @@ -1073,47 +781,6 @@ function formatItemProperties(item) { return properties.join(', '); } -/** - * Format item ratings for display (separate from properties) - */ -function formatItemRatings(item) { - const ratings = []; - - // Armor level - if (item.armor_level && item.armor_level > 0) { - ratings.push(`AL ${item.armor_level}`); - } - - // Damage ratings - if (item.crit_damage_rating && item.crit_damage_rating > 0) { - ratings.push(`CD +${item.crit_damage_rating}`); - } - - if (item.damage_rating && item.damage_rating > 0) { - ratings.push(`DR +${item.damage_rating}`); - } - - // Resist ratings - if (item.crit_damage_resist_rating && item.crit_damage_resist_rating > 0) { - ratings.push(`CDR +${item.crit_damage_resist_rating}`); - } - - if (item.damage_resist_rating && item.damage_resist_rating > 0) { - ratings.push(`DRR +${item.damage_resist_rating}`); - } - - // Other ratings - if (item.heal_boost_rating && item.heal_boost_rating > 0) { - ratings.push(`HB +${item.heal_boost_rating}`); - } - - if (item.vitality_rating && item.vitality_rating > 0) { - ratings.push(`VIT +${item.vitality_rating}`); - } - - return ratings.join(', '); -} - /** * Select a suit and populate the visual slots */ @@ -1154,7 +821,7 @@ function populateVisualSlots(items) { slotElement.innerHTML = `
${item.name}
-
${item.source_character || item.character_name || 'Unknown'}
+
${item.character_name}
${formatItemProperties(item)}
${needsReducing} `;