MosswartOverlord/static/suitbuilder.js
erik e7ca39318f Fix score-based ordering in suitbuilder frontend
Updated JavaScript to maintain score ordering during streaming search:
- Replace addSuitToResults() with insertSuitInScoreOrder()
- Add regenerateResultsDisplay() to maintain proper DOM ordering
- Medal assignment (🥇🥈🥉) now based on score ranking, not arrival order
- Suits with highest scores now always appear at top during live search
- Updated displaySuitResults() to sort by score before displaying

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 20:02:01 +00:00

1033 lines
No EOL
35 KiB
JavaScript

// 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 =
'<div class="error">Failed to load characters</div>';
}
}
/**
* Display characters in the selection panel
*/
function displayCharacters(characters) {
const characterList = document.getElementById('characterList');
if (!characters || characters.length === 0) {
characterList.innerHTML = '<div class="no-results">No characters found</div>';
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 += `
<div class="checkbox-item">
<input type="checkbox" id="char_${safeId}"
class="character-checkbox" value="${character}" checked>
<label for="char_${safeId}">${character}</label>
</div>
`;
});
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 = `
<div class="loading">
🔍 Searching for optimal suits...
<div class="search-progress">
<div class="progress-stats">
Found: <span id="foundCount">0</span> suits |
Checked: <span id="checkedCount">0</span> combinations |
Time: <span id="elapsedTime">0.0</span>s
</div>
<button id="stopSearch" class="stop-search-btn">Stop Search</button>
</div>
</div>
<div id="streamingResults"></div>
`;
countSpan.textContent = '';
try {
await streamOptimalSuits(constraints);
} catch (error) {
console.error('Suit search error:', error);
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
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 = `
<div class="suit-item" data-suit-id="${suit.id}">
<div class="suit-header">
<div class="suit-score ${scoreClass}">
${medal} Suit #${suit.id} (Score: ${suit.score})
</div>
</div>
<div class="suit-stats">
${formatSuitStats(suit)}
</div>
<div class="suit-items">
${formatSuitItems(suit.items)}
${suit.missing && suit.missing.length > 0 ? `<div class="missing-items">Missing: ${suit.missing.join(', ')}</div>` : ''}
${suit.notes && suit.notes.length > 0 ? `<div class="suit-notes">${suit.notes.join(' • ')}</div>` : ''}
</div>
</div>
`;
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 = '<div class="no-results">No suits found matching your constraints. Try relaxing some requirements.</div>';
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 += `
<div class="suit-item" data-suit-id="${suit.id}">
<div class="suit-header">
<div class="suit-score ${scoreClass}">
${medal} Suit #${suit.id} (Score: ${suit.score}%)
</div>
</div>
<div class="suit-stats">
${formatSuitStats(suit)}
</div>
<div class="suit-items">
${formatSuitItems(suit.items)}
${suit.missing && suit.missing.length > 0 ? `<div class="missing-items">Missing: ${suit.missing.join(', ')}</div>` : ''}
${suit.notes && suit.notes.length > 0 ? `<div class="suit-notes">${suit.notes.join(' • ')}</div>` : ''}
</div>
</div>
`;
});
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) ? '<span class="need-reducing">Need Reducing</span>' : '';
const properties = formatItemProperties(item);
const ratings = formatItemRatings(item);
html += `
<div class="suit-item-entry">
<strong>${slot}:</strong>
<span class="item-character">${item.source_character || item.character_name || 'Unknown'}</span> -
<span class="item-name">${item.name}</span>
${properties ? `<span class="item-properties">(${properties})</span>` : ''}
${ratings ? `<span class="item-ratings">[${ratings}]</span>` : ''}
${needsReducing}
</div>
`;
} else {
// Empty slot
html += `
<div class="suit-item-entry empty-slot">
<strong>${slot}:</strong>
<span class="empty-slot-text">- Empty -</span>
</div>
`;
}
});
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 ? `<div class="suit-stats-line">${statParts.join(' • ')}</div>` : '';
}
/**
* 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 = '<span class="empty-slot">Empty</span>';
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) ? '<span class="need-reducing">Need Reducing</span>' : '';
slotElement.innerHTML = `
<div class="slot-item-name">${item.name}</div>
<div class="slot-item-character">${item.character_name}</div>
<div class="slot-item-properties">${formatItemProperties(item)}</div>
${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 = '<span class="empty-slot">Empty</span>';
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 =
'<div class="no-results">Configure constraints above and click "Search Suits" to find optimal loadouts.</div>';
document.getElementById('resultsCount').textContent = '';
// Reset slots
resetSlotView();
}