// 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 += `
`;
}
});
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.