936 lines
No EOL
31 KiB
JavaScript
936 lines
No EOL
31 KiB
JavaScript
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
|
|
|
|
// Configuration
|
|
const API_BASE = '/inv';
|
|
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/list`);
|
|
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 => {
|
|
html += `
|
|
<div class="checkbox-item">
|
|
<input type="checkbox" id="char_${character.character_name}"
|
|
class="character-checkbox" value="${character.character_name}" checked>
|
|
<label for="char_${character.character_name}">${character.character_name}</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) {
|
|
// Build request parameters for the streaming constraint solver
|
|
const params = new URLSearchParams();
|
|
|
|
// Character selection
|
|
if (constraints.characters.length > 0) {
|
|
params.append('characters', constraints.characters.join(','));
|
|
} else {
|
|
params.append('include_all_characters', 'true');
|
|
}
|
|
|
|
// Equipment sets
|
|
if (constraints.primary_set) {
|
|
params.append('primary_set', constraints.primary_set);
|
|
}
|
|
if (constraints.secondary_set) {
|
|
params.append('secondary_set', constraints.secondary_set);
|
|
}
|
|
|
|
// 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);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const eventSource = new EventSource(streamUrl);
|
|
let searchStopped = false;
|
|
|
|
// Add stop search functionality
|
|
const stopButton = document.getElementById('stopSearch');
|
|
stopButton.addEventListener('click', () => {
|
|
searchStopped = true;
|
|
eventSource.close();
|
|
|
|
// 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)`;
|
|
}
|
|
|
|
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'));
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add a single suit to the streaming results display
|
|
*/
|
|
function addSuitToResults(suit, index) {
|
|
const streamingResults = document.getElementById('streamingResults');
|
|
if (!streamingResults) return;
|
|
|
|
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);
|
|
|
|
// Add click handler for the new suit
|
|
const newSuitElement = streamingResults.lastElementChild;
|
|
newSuitElement.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;
|
|
}
|
|
|
|
countSpan.textContent = `Found ${suits.length} suit${suits.length !== 1 ? 's' : ''}`;
|
|
|
|
let html = '';
|
|
suits.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
|
|
*/
|
|
function formatSuitItems(items) {
|
|
let html = '';
|
|
|
|
if (!items || Object.keys(items).length === 0) {
|
|
return '<div class="no-items">No items in this suit</div>';
|
|
}
|
|
|
|
Object.entries(items).forEach(([slot, item]) => {
|
|
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
|
|
const properties = formatItemProperties(item);
|
|
|
|
html += `
|
|
<div class="suit-item-entry">
|
|
<strong>${slot}:</strong>
|
|
<span class="item-character">${item.character_name}</span> -
|
|
<span class="item-name">${item.name}</span>
|
|
${properties ? `<span class="item-properties">(${properties})</span>` : ''}
|
|
${needsReducing}
|
|
</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(', ');
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
} |