// Suitbuilder JavaScript - Constraint Solver Frontend Logic // Configuration const API_BASE = '/inv/suitbuilder'; let currentSuits = []; let lockedSlots = new Set(); let selectedSuit = null; // Initialize when page loads document.addEventListener('DOMContentLoaded', function() { initializeSuitbuilder(); }); /** * Initialize all suitbuilder functionality */ function initializeSuitbuilder() { loadCharacters(); setupEventListeners(); setupSlotInteractions(); } /** * Load available characters for selection */ async function loadCharacters() { try { const response = await fetch(`${API_BASE}/characters`); if (!response.ok) { throw new Error('Failed to load characters'); } const data = await response.json(); displayCharacters(data.characters); } catch (error) { console.error('Error loading characters:', error); document.getElementById('characterList').innerHTML = '
Failed to load characters
'; } } /** * Display characters in the selection panel */ function displayCharacters(characters) { const characterList = document.getElementById('characterList'); if (!characters || characters.length === 0) { characterList.innerHTML = '
No characters found
'; return; } 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 += `
`; }); characterList.innerHTML = html; // Setup character checkbox interactions setupCharacterCheckboxes(); } /** * Setup character checkbox interactions */ function setupCharacterCheckboxes() { const allCheckbox = document.getElementById('char_all'); const characterCheckboxes = document.querySelectorAll('.character-checkbox:not([value="all"])'); // "All Characters" checkbox toggle allCheckbox.addEventListener('change', function() { characterCheckboxes.forEach(cb => { cb.checked = this.checked; }); }); // Individual character checkbox changes characterCheckboxes.forEach(cb => { cb.addEventListener('change', function() { // Update "All Characters" checkbox state const checkedCount = Array.from(characterCheckboxes).filter(cb => cb.checked).length; allCheckbox.checked = checkedCount === characterCheckboxes.length; allCheckbox.indeterminate = checkedCount > 0 && checkedCount < characterCheckboxes.length; }); }); } /** * Setup all event listeners */ function setupEventListeners() { // Main action buttons document.getElementById('searchSuits').addEventListener('click', performSuitSearch); document.getElementById('clearAll').addEventListener('click', clearAllConstraints); // Slot control buttons document.getElementById('lockSelectedSlots').addEventListener('click', lockSelectedSlots); document.getElementById('clearAllLocks').addEventListener('click', clearAllLocks); document.getElementById('resetSlotView').addEventListener('click', resetSlotView); } /** * Setup slot interaction functionality */ function setupSlotInteractions() { // Lock button interactions document.querySelectorAll('.lock-btn').forEach(btn => { btn.addEventListener('click', function() { const slot = this.dataset.slot; toggleSlotLock(slot); }); }); // Slot item click interactions document.querySelectorAll('.slot-item').forEach(item => { item.addEventListener('click', function() { const slot = this.dataset.slot; handleSlotClick(slot); }); }); } /** * Perform suit search with current constraints using streaming results */ async function performSuitSearch() { const constraints = gatherConstraints(); if (!validateConstraints(constraints)) { return; } const resultsDiv = document.getElementById('suitResults'); const countSpan = document.getElementById('resultsCount'); // Reset current suits and UI currentSuits = []; resultsDiv.innerHTML = `
🔍 Searching for optimal suits...
Found: 0 suits | Checked: 0 combinations | Time: 0.0s
`; countSpan.textContent = ''; try { await streamOptimalSuits(constraints); } catch (error) { console.error('Suit search error:', error); resultsDiv.innerHTML = `
❌ Suit search failed: ${error.message}
`; countSpan.textContent = ''; } } /** * Gather all current constraints from the form */ function gatherConstraints() { // Get selected characters const selectedCharacters = Array.from(document.querySelectorAll('.character-checkbox:checked:not([value="all"])')) .map(cb => cb.value); // Get rating constraints const constraints = { characters: selectedCharacters, min_armor: document.getElementById('minArmor').value || null, max_armor: document.getElementById('maxArmor').value || null, min_crit_damage: document.getElementById('minCritDmg').value || null, max_crit_damage: document.getElementById('maxCritDmg').value || null, min_damage_rating: document.getElementById('minDmgRating').value || null, max_damage_rating: document.getElementById('maxDmgRating').value || null, // Equipment status include_equipped: document.getElementById('includeEquipped').checked, include_inventory: document.getElementById('includeInventory').checked, // Equipment sets primary_set: document.getElementById('primarySet').value || null, secondary_set: document.getElementById('secondarySet').value || null, // Legendary cantrips (from cantrips-grid only) legendary_cantrips: Array.from(document.querySelectorAll('.cantrips-grid input:checked')) .map(cb => cb.value) .filter(value => value.includes('Legendary')), // Legendary wards (separate section) protection_spells: Array.from(document.querySelectorAll('#protection_flame, #protection_frost, #protection_acid, #protection_storm, #protection_slashing, #protection_piercing, #protection_bludgeoning, #protection_armor')) .filter(cb => cb.checked) .map(cb => cb.value), // Locked slots locked_slots: Array.from(lockedSlots) }; return constraints; } /** * Validate constraints before search */ function validateConstraints(constraints) { if (!constraints.characters || constraints.characters.length === 0) { alert('Please select at least one character.'); return false; } if (!constraints.primary_set && !constraints.secondary_set && constraints.legendary_cantrips.length === 0 && constraints.protection_spells.length === 0 && !constraints.min_armor && !constraints.min_crit_damage && !constraints.min_damage_rating) { alert('Please specify at least one constraint (equipment sets, cantrips, legendary wards, or rating minimums).'); return false; } return true; } /** * 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 }; console.log('Starting suit search with constraints:', requestBody); // 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) }); if (!response.ok) { throw new Error(`Search failed: ${response.statusText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); return new Promise((resolve, reject) => { 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) { reject(error); } } readStream(); // Event handlers function handleSuitEvent(data) { try { // Transform backend suit format to frontend format const transformedSuit = transformSuitData(data); // Insert suit in score-ordered position (highest score first) insertSuitInScoreOrder(transformedSuit); // Regenerate entire results display to maintain proper ordering regenerateResultsDisplay(); // 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); } } 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; // Update UI to show search was stopped const loadingDiv = document.querySelector('.loading'); if (loadingDiv) { loadingDiv.innerHTML = 'âšī¸ Search stopped by user.'; } // Update results count const countSpan = document.getElementById('resultsCount'); if (countSpan) { countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''} (search stopped)`; } }); }); } /** * Transform backend suit optimization response to frontend format */ function transformSuitsResponse(data) { if (!data.suits || data.suits.length === 0) { return []; } return data.suits.map(suit => { return transformSuitData(suit); }); } /** * Transform individual suit data from backend to frontend format */ function transformSuitData(suit) { return { id: suit.id || currentSuits.length + 1, score: Math.round(suit.score || 0), items: suit.items || {}, stats: suit.stats || {}, missing: suit.missing || [], notes: suit.notes || [], alternatives: [], primary_set: suit.stats?.primary_set || '', primary_set_count: suit.stats?.primary_set_count || 0, secondary_set: suit.stats?.secondary_set || '', secondary_set_count: suit.stats?.secondary_set_count || 0, total_armor: suit.stats?.total_armor || 0, total_crit_damage: suit.stats?.total_crit_damage || 0, total_damage_rating: suit.stats?.total_damage_rating || 0, spell_coverage: suit.stats?.spell_coverage || 0 }; } /** * Insert a suit into the currentSuits array in score-ordered position (highest first) */ function insertSuitInScoreOrder(suit) { // 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); } /** * Regenerate the entire results display to maintain proper score ordering */ function regenerateResultsDisplay() { 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); }); }); } /** * Generate suit combinations from available items * This is a simplified algorithm - the full constraint solver will be more sophisticated */ function generateSuitCombinations(itemsBySlot, constraints) { const suits = []; // For now, create a few example suits based on available items // This will be replaced with the full constraint solver algorithm // Try to build suits with the best items from each slot const armorSlots = ['Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands', 'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet']; const jewelrySlots = ['Neck', 'Left Ring', 'Right Ring', 'Left Wrist', 'Right Wrist', 'Trinket']; const clothingSlots = ['Shirt', 'Pants']; // Generate a few sample combinations for (let i = 0; i < Math.min(5, 20); i++) { const suit = { id: i + 1, score: Math.floor(Math.random() * 40) + 60, // Random score 60-100% items: {}, missing: [], alternatives: [] }; // Try to fill each slot with available items [...armorSlots, ...jewelrySlots, ...clothingSlots].forEach(slot => { const availableItems = itemsBySlot[slot]; if (availableItems && availableItems.length > 0) { // Pick the best item for this slot (simplified) const bestItem = availableItems[Math.floor(Math.random() * Math.min(3, availableItems.length))]; suit.items[slot] = bestItem; } }); // Calculate missing pieces based on set requirements if (constraints.primary_set || constraints.secondary_set) { suit.missing = calculateMissingPieces(suit.items, constraints); } // Only include suits that have at least some items if (Object.keys(suit.items).length > 0) { suits.push(suit); } } // Sort by score (best first) suits.sort((a, b) => b.score - a.score); return suits.slice(0, 10); // Return top 10 suits } /** * Calculate missing pieces for set requirements */ function calculateMissingPieces(suitItems, constraints) { const missing = []; if (constraints.primary_set) { const primaryItems = Object.values(suitItems).filter(item => item.set_name && item.set_name.includes(getSetNameById(constraints.primary_set)) ); if (primaryItems.length < 5) { missing.push(`${5 - primaryItems.length} more ${getSetNameById(constraints.primary_set)} pieces`); } } if (constraints.secondary_set) { const secondaryItems = Object.values(suitItems).filter(item => item.set_name && item.set_name.includes(getSetNameById(constraints.secondary_set)) ); if (secondaryItems.length < 4) { missing.push(`${4 - secondaryItems.length} more ${getSetNameById(constraints.secondary_set)} pieces`); } } return missing; } /** * Get set name by ID */ function getSetNameById(setId) { const setNames = { '13': "Soldier's", '14': "Adept's", '15': "Archer's", '16': "Defender's", '19': "Hearty", '20': "Dexterous", '21': "Wise", '22': "Swift", '24': "Reinforced", '26': "Flame Proof", '29': "Lightning Proof", '40': "Heroic Protector", '41': "Heroic Destroyer", '46': "Relic Alduressa", '47': "Ancient Relic", '48': "Noble Relic" }; return setNames[setId] || `Set ${setId}`; } /** * Display suit search results */ function displaySuitResults(suits) { const resultsDiv = document.getElementById('suitResults'); const countSpan = document.getElementById('resultsCount'); if (!suits || suits.length === 0) { resultsDiv.innerHTML = '
No suits found matching your constraints. Try relaxing some requirements.
'; countSpan.textContent = ''; 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' : ''}`; let html = ''; sortedSuits.forEach((suit, index) => { const scoreClass = getScoreClass(suit.score); const medal = index === 0 ? 'đŸĨ‡' : index === 1 ? 'đŸĨˆ' : index === 2 ? 'đŸĨ‰' : '🔸'; html += `
${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(' â€ĸ ')}
` : ''}
`; }); resultsDiv.innerHTML = html; // Add click handlers for suit selection document.querySelectorAll('.suit-item').forEach(item => { item.addEventListener('click', function() { const suitId = parseInt(this.dataset.suitId); selectSuit(suitId); }); }); } /** * Get CSS class for score */ function getScoreClass(score) { if (score >= 90) return 'excellent'; if (score >= 75) return 'good'; if (score >= 60) return 'fair'; return 'poor'; } /** * Format suit items for display - shows ALL armor slots even if empty */ function formatSuitItems(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' ]; allSlots.forEach(slot => { const item = items ? items[slot] : null; if (item) { // Item exists in this slot const needsReducing = isMultiSlotItem(item) ? 'Need Reducing' : ''; const properties = formatItemProperties(item); const ratings = formatItemRatings(item); html += `
${slot}: ${item.source_character || item.character_name || 'Unknown'} - ${item.name} ${properties ? `(${properties})` : ''} ${ratings ? `[${ratings}]` : ''} ${needsReducing}
`; } else { // Empty slot html += `
${slot}: - Empty -
`; } }); return html; } /** * Check if item is multi-slot and needs reducing * Only armor items need reduction - jewelry can naturally go in multiple slots */ function isMultiSlotItem(item) { if (!item.slot_name) return false; const slots = item.slot_name.split(',').map(s => s.trim()); if (slots.length <= 1) return false; // Jewelry items that can go in multiple equivalent slots (normal behavior, no reduction needed) const jewelryPatterns = [ ['Left Ring', 'Right Ring'], ['Left Wrist', 'Right Wrist'] ]; // Check if this matches any jewelry pattern for (const pattern of jewelryPatterns) { if (pattern.length === slots.length && pattern.every(slot => slots.includes(slot))) { return false; // This is jewelry, no reduction needed } } // If it has multiple slots and isn't jewelry, it's armor that needs reduction return true; } /** * Format suit statistics for display */ function formatSuitStats(suit) { if (!suit) return ''; const statParts = []; // Show set names with counts if (suit.primary_set && suit.primary_set_count > 0) { statParts.push(`${suit.primary_set}: ${suit.primary_set_count}/5`); } if (suit.secondary_set && suit.secondary_set_count > 0) { statParts.push(`${suit.secondary_set}: ${suit.secondary_set_count}/4`); } // Show total armor if (suit.total_armor > 0) { statParts.push(`Armor: ${suit.total_armor}`); } // Show spell coverage if (suit.spell_coverage > 0) { statParts.push(`Spells: ${suit.spell_coverage}`); } return statParts.length > 0 ? `
${statParts.join(' â€ĸ ')}
` : ''; } /** * Format item properties for display */ function formatItemProperties(item) { const properties = []; // Handle set name (backend sends item_set_name) if (item.item_set_name) { properties.push(item.item_set_name); } else if (item.set_name) { properties.push(item.set_name); } // Handle spells (backend sends spells array) const spellArray = item.spells || item.spell_names; if (spellArray && Array.isArray(spellArray)) { spellArray.forEach(spell => { if (spell.includes('Legendary')) { properties.push(spell); } }); } if (item.crit_damage_rating > 0) { properties.push(`Crit Dmg +${item.crit_damage_rating}`); } if (item.damage_rating > 0) { properties.push(`Dmg Rating +${item.damage_rating}`); } if (item.heal_boost > 0) { properties.push(`Heal Boost +${item.heal_boost}`); } 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 */ function selectSuit(suitId) { const suit = currentSuits.find(s => s.id === suitId); if (!suit) return; // Update visual selection document.querySelectorAll('.suit-item').forEach(item => { item.classList.remove('selected'); }); document.querySelector(`[data-suit-id="${suitId}"]`).classList.add('selected'); // Populate visual slots populateVisualSlots(suit.items); selectedSuit = suit; } /** * Populate the visual equipment slots with suit items */ function populateVisualSlots(items) { // Clear all slots first document.querySelectorAll('.slot-content').forEach(slot => { const slotName = slot.id.replace('slot_', '').replace('_', ' '); slot.innerHTML = 'Empty'; slot.parentElement.classList.remove('populated'); }); // Populate with items Object.entries(items).forEach(([slotName, item]) => { const slotId = `slot_${slotName.replace(' ', '_')}`; const slotElement = document.getElementById(slotId); if (slotElement) { const needsReducing = isMultiSlotItem(item) ? 'Need Reducing' : ''; slotElement.innerHTML = `
${item.name}
${item.character_name}
${formatItemProperties(item)}
${needsReducing} `; slotElement.parentElement.classList.add('populated'); } }); } /** * Toggle lock state of a slot */ function toggleSlotLock(slotName) { const slotElement = document.querySelector(`[data-slot="${slotName}"]`); const lockBtn = slotElement.querySelector('.lock-btn'); if (lockedSlots.has(slotName)) { lockedSlots.delete(slotName); slotElement.classList.remove('locked'); lockBtn.classList.remove('locked'); } else { lockedSlots.add(slotName); slotElement.classList.add('locked'); lockBtn.classList.add('locked'); } } /** * Handle slot click events */ function handleSlotClick(slotName) { // For now, just toggle lock state // Later this could open item selection dialog console.log(`Clicked slot: ${slotName}`); } /** * Lock currently selected slots */ function lockSelectedSlots() { document.querySelectorAll('.slot-item.populated').forEach(slot => { const slotName = slot.dataset.slot; if (!lockedSlots.has(slotName)) { toggleSlotLock(slotName); } }); } /** * Clear all slot locks */ function clearAllLocks() { lockedSlots.clear(); document.querySelectorAll('.slot-item').forEach(slot => { slot.classList.remove('locked'); slot.querySelector('.lock-btn').classList.remove('locked'); }); } /** * Reset the slot view */ function resetSlotView() { clearAllLocks(); document.querySelectorAll('.slot-content').forEach(slot => { const slotName = slot.id.replace('slot_', '').replace('_', ' '); slot.innerHTML = 'Empty'; slot.parentElement.classList.remove('populated'); }); selectedSuit = null; // Clear suit selection document.querySelectorAll('.suit-item').forEach(item => { item.classList.remove('selected'); }); } /** * Clear all constraints and reset form */ function clearAllConstraints() { // Clear all input fields document.querySelectorAll('input[type="number"]').forEach(input => { input.value = ''; }); // Reset checkboxes document.querySelectorAll('input[type="checkbox"]').forEach(cb => { if (cb.id === 'char_all' || cb.id === 'includeEquipped' || cb.id === 'includeInventory') { cb.checked = true; } else { cb.checked = false; } }); // Reset dropdowns document.querySelectorAll('select').forEach(select => { select.selectedIndex = 0; }); // Reset character selection document.querySelectorAll('.character-checkbox:not([value="all"])').forEach(cb => { cb.checked = true; }); // Clear results document.getElementById('suitResults').innerHTML = '
Configure constraints above and click "Search Suits" to find optimal loadouts.
'; document.getElementById('resultsCount').textContent = ''; // Reset slots resetSlotView(); }