- -
-
- -
- - - -
-
-
- -
-
- - +
+
+ + +
+
+ +
+ + + + + +
+
+
+ + + +
+
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 f828e093..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/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() { @@ -163,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 = ''; + } } } @@ -260,13 +267,22 @@ async function streamOptimalSuits(constraints) { console.log('Starting suit search with constraints:', requestBody); + // Cancel any existing search + if (currentSearchController) { + currentSearchController.abort(); + } + + // 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) + body: JSON.stringify(requestBody), + signal: currentSearchController.signal // Add abort signal }); if (!response.ok) { @@ -340,7 +356,13 @@ async function streamOptimalSuits(constraints) { } } } catch (error) { - reject(error); + // Don't treat abort as an error + if (error.name === 'AbortError') { + console.log('Search was aborted by user'); + resolve(); + } else { + reject(error); + } } } @@ -349,14 +371,19 @@ async function streamOptimalSuits(constraints) { // 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) - insertSuitInScoreOrder(transformedSuit); + const insertIndex = insertSuitInScoreOrder(transformedSuit); + console.log('Insert index returned:', insertIndex); - // Regenerate entire results display to maintain proper ordering - regenerateResultsDisplay(); + // 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; @@ -364,6 +391,7 @@ async function streamOptimalSuits(constraints) { } catch (error) { console.error('Error processing suit data:', error); + console.error('Stack trace:', error.stack); } } @@ -412,6 +440,12 @@ async function streamOptimalSuits(constraints) { stopButton.addEventListener('click', () => { searchStopped = true; + // Actually abort the HTTP request + if (currentSearchController) { + currentSearchController.abort(); + currentSearchController = null; + } + // Update UI to show search was stopped const loadingDiv = document.querySelector('.loading'); if (loadingDiv) { @@ -467,6 +501,8 @@ function transformSuitData(suit) { * Insert a suit into the currentSuits array in score-ordered position (highest first) */ 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++) { @@ -479,12 +515,17 @@ function insertSuitInScoreOrder(suit) { // 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; @@ -526,6 +567,79 @@ function regenerateResultsDisplay() { }); } +/** + * 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 = ` +
+
+
+ 🔸 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(' • ')}
` : ''} +
+
+ `; + + // 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.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 @@ -697,7 +811,7 @@ function getScoreClass(score) { * 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); // Define all expected armor/equipment slots in logical order const allSlots = [ @@ -710,39 +824,161 @@ function formatSuitItems(items) { 'Shirt', 'Pants' ]; + 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 needsReducing = isMultiSlotItem(item) ? 'Need Reducing' : ''; - const properties = formatItemProperties(item); - const ratings = formatItemRatings(item); + 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}: - ${item.source_character || item.character_name || 'Unknown'} - - ${item.name} - ${properties ? `(${properties})` : ''} - ${ratings ? `[${ratings}]` : ''} - ${needsReducing} +
+
${slot}
+
${character}
+
${itemName}${needsReducing}
+
${setName}
+
${spells}
+
${armor}
+
${ratings}
`; } else { // Empty slot html += ` -
- ${slot}: - - Empty - +
+
${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 @@ -918,7 +1154,7 @@ function populateVisualSlots(items) { slotElement.innerHTML = `
${item.name}
-
${item.character_name}
+
${item.source_character || item.character_name || 'Unknown'}
${formatItemProperties(item)}
${needsReducing} `;