- -
-
- -
- - - -
-
-
- -
-
- - +
+
+ + +
+
+ +
+ + + + + +
+
+
+ + + +
+
Item Stats
+
+
+ + + - + +
+
+ + + - + +
-
- - + +
+
+ + + - + +
+
+ + + - + +
-
- - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - -
-
- - -
-
- - + +
+
+ + +
+
+ + +
+
+ + +
- -
- -
+ +
+ +
+
Equipment Sets
+
@@ -605,11 +666,11 @@
-
+
- -
- + +
+
Legendary Cantrips
@@ -782,11 +843,11 @@
-
+
- -
- + +
+
Legendary Wards
@@ -821,15 +882,12 @@
-
+
- -
- - - -
- + +
+
Equipment Slots
+
@@ -870,11 +928,6 @@
-
- - -
-
@@ -892,6 +945,7 @@
+
diff --git a/static/inventory.js b/static/inventory.js index 77469a81..e77978fa 100644 --- a/static/inventory.js +++ b/static/inventory.js @@ -255,6 +255,10 @@ 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 @@ -298,6 +302,7 @@ 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'); @@ -438,6 +443,7 @@ 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')} @@ -451,6 +457,7 @@ 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'; @@ -522,6 +529,7 @@ function displayResults(data) { ${healBoostRating} ${vitalityRating} ${damageResistRating} + ${critDamageResistRating} ${lastUpdated} `; diff --git a/static/suitbuilder.css b/static/suitbuilder.css index 73d3577c..3fe8a8d0 100644 --- a/static/suitbuilder.css +++ b/static/suitbuilder.css @@ -657,6 +657,176 @@ 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 2c652c10..47a9a842 100644 --- a/static/suitbuilder.js +++ b/static/suitbuilder.js @@ -1,10 +1,12 @@ // Suitbuilder JavaScript - Constraint Solver Frontend Logic +console.log('Suitbuilder.js loaded - VERSION: SCORE_ORDERING_AND_CANCELLATION_FIX_v3'); // Configuration -const API_BASE = '/inv'; +const API_BASE = '/inv/suitbuilder'; let currentSuits = []; let lockedSlots = new Set(); let selectedSuit = null; +let currentSearchController = null; // AbortController for current search // Initialize when page loads document.addEventListener('DOMContentLoaded', function() { @@ -25,7 +27,7 @@ function initializeSuitbuilder() { */ async function loadCharacters() { try { - const response = await fetch(`${API_BASE}/characters/list`); + const response = await fetch(`${API_BASE}/characters`); if (!response.ok) { throw new Error('Failed to load characters'); } @@ -52,11 +54,13 @@ 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 += `
- - + +
`; }); @@ -161,9 +165,14 @@ async function performSuitSearch() { try { await streamOptimalSuits(constraints); } catch (error) { - console.error('Suit search error:', error); - resultsDiv.innerHTML = `
❌ Suit search failed: ${error.message}
`; - countSpan.textContent = ''; + // 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 = ''; + } } } @@ -234,66 +243,208 @@ function validateConstraints(constraints) { * Stream optimal suits using Server-Sent Events with progressive results */ async function streamOptimalSuits(constraints) { - // Build request parameters for the streaming constraint solver - const params = new URLSearchParams(); + // 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 + }; - // Character selection - if (constraints.characters.length > 0) { - params.append('characters', constraints.characters.join(',')); - } else { - params.append('include_all_characters', 'true'); + console.log('Starting suit search with constraints:', requestBody); + + // Cancel any existing search + if (currentSearchController) { + currentSearchController.abort(); } - // Equipment sets - if (constraints.primary_set) { - params.append('primary_set', constraints.primary_set); - } - if (constraints.secondary_set) { - params.append('secondary_set', constraints.secondary_set); + // 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}`); } - // 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); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); 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; - eventSource.close(); + + // Actually abort the HTTP request + if (currentSearchController) { + currentSearchController.abort(); + currentSearchController = null; + } // Update UI to show search was stopped const loadingDiv = document.querySelector('.loading'); @@ -306,118 +457,7 @@ 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')); - } - }; }); } @@ -458,20 +498,96 @@ function transformSuitData(suit) { } /** - * Add a single suit to the streaming results display + * Insert a suit into the currentSuits array in score-ordered position (highest first) */ -function addSuitToResults(suit, index) { +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}`)); + const streamingResults = document.getElementById('streamingResults'); if (!streamingResults) return; - const scoreClass = getScoreClass(suit.score); - const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸'; + // 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 suitHtml = `
- ${medal} Suit #${suit.id} (Score: ${suit.score}) + 🔸 Suit #${suit.id} (Score: ${suit.score})
@@ -485,16 +601,45 @@ function addSuitToResults(suit, index) {
`; - streamingResults.insertAdjacentHTML('beforeend', suitHtml); + // 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(); // Add click handler for the new suit - const newSuitElement = streamingResults.lastElementChild; + const newSuitElement = streamingResults.children[insertIndex]; 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 @@ -612,10 +757,13 @@ function displaySuitResults(suits) { return; } - countSpan.textContent = `Found ${suits.length} suit${suits.length !== 1 ? 's' : ''}`; + // 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' : ''}`; let html = ''; - suits.forEach((suit, index) => { + sortedSuits.forEach((suit, index) => { const scoreClass = getScoreClass(suit.score); const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸'; @@ -660,33 +808,177 @@ function getScoreClass(score) { } /** - * Format suit items for display + * Format suit items for display - shows ALL armor slots even if empty */ function formatSuitItems(items) { - let html = ''; + console.log(`[DEBUG] formatSuitItems called with items:`, items); - if (!items || Object.keys(items).length === 0) { - return '
No items in this suit
'; - } + // 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' + ]; - 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} + console.log(`[DEBUG] allSlots:`, allSlots); + + // Create table structure with header + let html = ` +
+
+
Slot
+
Character
+
Item
+
Set
+
Spells
+
Armor
+
Ratings
- `; +
+ `; + + 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 @@ -781,6 +1073,47 @@ 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 */ @@ -821,7 +1154,7 @@ function populateVisualSlots(items) { slotElement.innerHTML = `
${item.name}
-
${item.character_name}
+
${item.source_character || item.character_name || 'Unknown'}
${formatItemProperties(item)}
${needsReducing} `;