Add suitbuilder UI improvements for locked slots and suit summary

- Expand locked item spell selection to include weapon skills, magic skills, and defenses
- Preserve locked slot configuration when selecting different suits
- Add clear button (x) to individual equipment slots for granular control
- Add suit summary section below equipment slots with copy-to-clipboard functionality

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-05 19:11:37 +00:00
parent 0fd539bedf
commit 8e70f88de1
3 changed files with 1391 additions and 110 deletions

View file

@ -1,13 +1,54 @@
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
console.log('Suitbuilder.js loaded - VERSION: SCORE_ORDERING_AND_CANCELLATION_FIX_v3');
console.log('Suitbuilder.js loaded - VERSION: CONSISTENT_SUIT_COUNT_v4');
// Configuration
const API_BASE = '/inv/suitbuilder';
let currentSuits = [];
let lockedSlots = new Set();
let lockedSlots = new Map(); // slot -> { set: string|null, setId: number|null, spells: string[] }
let selectedSuit = null;
let currentSearchController = null; // AbortController for current search
// Common legendary cantrips for lock form
const COMMON_CANTRIPS = [
// Attributes
'Legendary Strength',
'Legendary Endurance',
'Legendary Coordination',
'Legendary Quickness',
'Legendary Focus',
'Legendary Willpower',
// Weapon Skills
'Legendary Finesse Weapon Aptitude',
'Legendary Heavy Weapon Aptitude',
'Legendary Light Weapon Aptitude',
'Legendary Missile Weapon Aptitude',
'Legendary Two Handed Combat Aptitude',
// Magic Skills
'Legendary War Magic Aptitude',
'Legendary Void Magic Aptitude',
'Legendary Creature Enchantment Aptitude',
'Legendary Item Enchantment Aptitude',
'Legendary Life Magic Aptitude',
// Defense
'Legendary Magic Resistance',
'Legendary Invulnerability'
];
// Common legendary wards for lock form
const COMMON_WARDS = [
'Legendary Flame Ward',
'Legendary Frost Ward',
'Legendary Acid Ward',
'Legendary Storm Ward',
'Legendary Slashing Ward',
'Legendary Piercing Ward',
'Legendary Bludgeoning Ward',
'Legendary Armor'
];
// Armor slots (have equipment sets)
const ARMOR_SLOTS = ['Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands', 'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet'];
// Initialize when page loads
document.addEventListener('DOMContentLoaded', function() {
initializeSuitbuilder();
@ -134,34 +175,97 @@ function setupSlotInteractions() {
/**
* Perform suit search with current constraints using streaming results
*/
// Global timer for search
let searchStartTime = null;
let searchTimerInterval = null;
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
// Start timer IMMEDIATELY
searchStartTime = Date.now();
if (searchTimerInterval) clearInterval(searchTimerInterval);
// Reset current suits and UI with new fancy template
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 class="search-status-container">
<!-- Phase indicator with animated icon -->
<div class="phase-indicator">
<div class="phase-icon" id="phaseIcon"></div>
<div class="phase-text" id="searchPhase">Initializing...</div>
</div>
<!-- Progress bars -->
<div class="progress-bars">
<div class="progress-bar-container">
<label>Initialization</label>
<div class="progress-bar">
<div class="progress-fill phase-progress" id="phaseProgress" style="width: 0%"></div>
</div>
</div>
<div class="progress-bar-container" id="bucketProgressContainer" style="display: none;">
<label>Search Progress</label>
<div class="progress-bar">
<div class="progress-fill bucket-progress" id="bucketProgress" style="width: 0%"></div>
</div>
<span class="progress-label" id="currentBucket"></span>
</div>
</div>
<!-- Stats grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Time</div>
<div class="stat-value" id="elapsedTime">0.0</div>
<div class="stat-unit">seconds</div>
</div>
<div class="stat-card">
<div class="stat-label">Evaluated</div>
<div class="stat-value" id="checkedCount">0</div>
<div class="stat-unit">combinations</div>
</div>
<div class="stat-card">
<div class="stat-label">Rate</div>
<div class="stat-value" id="searchRate">-</div>
<div class="stat-unit">per second</div>
</div>
<div class="stat-card highlight">
<div class="stat-label">Found</div>
<div class="stat-value" id="foundCount">0</div>
<div class="stat-unit">suits</div>
</div>
</div>
<!-- Verbose log area -->
<details class="verbose-log-section" open>
<summary>Verbose Output <span class="log-count" id="logCount">0</span></summary>
<div class="verbose-log" id="verboseLog"></div>
</details>
<!-- Stop button -->
<button id="stopSearch" class="stop-search-btn">
<span class="stop-icon"></span> Stop Search
</button>
</div>
<div id="streamingResults"></div>
`;
countSpan.textContent = '';
// Start client-side timer update (every 100ms for responsive display)
searchTimerInterval = setInterval(() => {
const elapsed = (Date.now() - searchStartTime) / 1000;
const timeEl = document.getElementById('elapsedTime');
if (timeEl) timeEl.textContent = elapsed.toFixed(1);
}, 100);
try {
await streamOptimalSuits(constraints);
} catch (error) {
@ -173,6 +277,12 @@ async function performSuitSearch() {
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
countSpan.textContent = '';
}
} finally {
// Stop client-side timer
if (searchTimerInterval) {
clearInterval(searchTimerInterval);
searchTimerInterval = null;
}
}
}
@ -212,10 +322,18 @@ function gatherConstraints() {
.filter(cb => cb.checked)
.map(cb => cb.value),
// Locked slots
locked_slots: Array.from(lockedSlots)
// Locked slots - convert Map to object for API
locked_slots: Object.fromEntries(
Array.from(lockedSlots.entries()).map(([slot, info]) => [
slot,
{
set_id: info.setId || null,
spells: info.spells || []
}
])
)
};
return constraints;
}
@ -252,7 +370,7 @@ async function streamOptimalSuits(constraints) {
...constraints.legendary_cantrips,
...constraints.protection_spells
],
locked_items: {}, // TODO: implement locked items
locked_slots: constraints.locked_slots || {},
include_equipped: constraints.include_equipped,
include_inventory: constraints.include_inventory,
min_armor: constraints.min_armor ? parseInt(constraints.min_armor) : null,
@ -336,6 +454,10 @@ async function streamOptimalSuits(constraints) {
handleSuitEvent(eventData);
} else if (currentEventType === 'progress') {
handleProgressEvent(eventData);
} else if (currentEventType === 'phase') {
handlePhaseEvent(eventData);
} else if (currentEventType === 'log') {
handleLogEvent(eventData);
} else if (currentEventType === 'complete') {
handleCompleteEvent(eventData);
resolve();
@ -372,23 +494,32 @@ async function streamOptimalSuits(constraints) {
function handleSuitEvent(data) {
try {
console.log('NEW handleSuitEvent called with data:', data);
// Transform backend suit format to frontend format
const transformedSuit = transformSuitData(data);
console.log('Transformed suit:', transformedSuit);
// Insert suit in score-ordered position (highest score first)
const insertIndex = insertSuitInScoreOrder(transformedSuit);
console.log('Insert index returned:', insertIndex);
// Insert DOM element at the correct position instead of regenerating everything
// If suit wasn't inserted (not good enough for top N), skip DOM update
if (insertIndex === -1) {
console.log('Suit not inserted, skipping DOM update');
return;
}
// Insert DOM element at the correct position
insertSuitDOMAtPosition(transformedSuit, insertIndex);
console.log('DOM insertion complete');
// Trim excess suits from array and DOM to maintain consistent count
trimExcessSuits();
// 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);
console.error('Stack trace:', error.stack);
@ -397,28 +528,165 @@ async function streamOptimalSuits(constraints) {
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';
// Always show frontend's actual suit count for consistency
document.getElementById('foundCount').textContent = currentSuits.length;
document.getElementById('checkedCount').textContent = (data.evaluated || 0).toLocaleString();
// Update rate display
const rateEl = document.getElementById('searchRate');
if (rateEl && data.rate !== undefined) {
rateEl.textContent = data.rate.toLocaleString();
}
// Update bucket progress bar
if (data.current_depth !== undefined && data.total_buckets) {
const bucketContainer = document.getElementById('bucketProgressContainer');
const bucketProgress = document.getElementById('bucketProgress');
const currentBucket = document.getElementById('currentBucket');
if (bucketContainer) bucketContainer.style.display = 'block';
if (bucketProgress) {
const progress = (data.current_depth / data.total_buckets) * 100;
bucketProgress.style.width = `${progress}%`;
}
if (currentBucket && data.current_bucket) {
currentBucket.textContent = `Searching: ${data.current_bucket} (${data.current_depth + 1}/${data.total_buckets})`;
}
}
// Note: Timer is now handled client-side for immediate response
} catch (error) {
console.error('Error processing progress data:', error);
}
}
function handlePhaseEvent(data) {
try {
const phaseIcon = document.getElementById('phaseIcon');
const phaseText = document.getElementById('searchPhase');
const phaseProgress = document.getElementById('phaseProgress');
// Phase icons
const icons = {
'loading': '📂',
'loaded': '✅',
'buckets': '🗂️',
'buckets_done': '✅',
'reducing': '✂️',
'sorting': '📊',
'searching': '🔍'
};
if (phaseIcon) {
phaseIcon.textContent = icons[data.phase] || '⏳';
// Add/remove animation for searching phase
if (data.phase === 'searching') {
phaseIcon.classList.add('searching');
}
}
if (phaseText) {
phaseText.textContent = data.message || data.phase;
}
// Update initialization progress bar
if (phaseProgress && data.phase_number && data.total_phases) {
const progress = (data.phase_number / data.total_phases) * 100;
phaseProgress.style.width = `${progress}%`;
// Make it indeterminate during searching
if (data.phase === 'searching') {
phaseProgress.classList.add('indeterminate');
}
}
// Log the phase change
addLogEntry(data.message, 'phase');
} catch (error) {
console.error('Error processing phase data:', error);
}
}
function handleLogEvent(data) {
try {
addLogEntry(data.message, data.level || 'info', data.timestamp);
} catch (error) {
console.error('Error processing log data:', error);
}
}
function addLogEntry(message, level = 'info', timestamp = null) {
const logContainer = document.getElementById('verboseLog');
const logCount = document.getElementById('logCount');
if (!logContainer) return;
const elapsed = timestamp || ((Date.now() - searchStartTime) / 1000);
const entry = document.createElement('div');
entry.className = `log-entry log-${level}`;
entry.innerHTML = `<span class="log-time">[${elapsed.toFixed(1)}s]</span> <span class="log-message">${message}</span>`;
logContainer.appendChild(entry);
// Trim old entries (max 100)
while (logContainer.children.length > 100) {
logContainer.removeChild(logContainer.firstChild);
}
// Update log count
if (logCount) {
logCount.textContent = logContainer.children.length;
}
// Auto-scroll to bottom
logContainer.scrollTop = logContainer.scrollHeight;
}
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.`;
const duration = ((Date.now() - searchStartTime) / 1000).toFixed(1);
// Update phase indicator to complete
const phaseIcon = document.getElementById('phaseIcon');
const phaseText = document.getElementById('searchPhase');
const phaseProgress = document.getElementById('phaseProgress');
if (phaseIcon) {
phaseIcon.textContent = '✅';
phaseIcon.classList.remove('searching');
phaseIcon.classList.add('complete');
}
if (phaseText) {
phaseText.textContent = `Search complete! Found ${currentSuits.length} suits`;
phaseText.classList.add('complete');
}
if (phaseProgress) {
phaseProgress.classList.remove('indeterminate');
phaseProgress.style.width = '100%';
}
// Fill bucket progress bar
const bucketProgress = document.getElementById('bucketProgress');
if (bucketProgress) {
bucketProgress.style.width = '100%';
}
// Hide stop button
const stopBtn = document.getElementById('stopSearch');
if (stopBtn) {
stopBtn.style.display = 'none';
}
// Add completion log entry
addLogEntry(`Search complete: ${currentSuits.length} suits found in ${duration}s`, 'success');
// 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);
}
@ -439,19 +707,36 @@ async function streamOptimalSuits(constraints) {
const stopButton = document.getElementById('stopSearch');
stopButton.addEventListener('click', () => {
searchStopped = true;
// Actually abort the HTTP request
if (currentSearchController) {
currentSearchController.abort();
currentSearchController = null;
}
// Update UI to show search was stopped
const loadingDiv = document.querySelector('.loading');
if (loadingDiv) {
loadingDiv.innerHTML = '⏹️ Search stopped by user.';
// Update phase indicator to show stopped
const phaseIcon = document.getElementById('phaseIcon');
const phaseText = document.getElementById('searchPhase');
const phaseProgress = document.getElementById('phaseProgress');
if (phaseIcon) {
phaseIcon.textContent = '⏹️';
phaseIcon.classList.remove('searching');
}
if (phaseText) {
phaseText.textContent = `Search stopped - Found ${currentSuits.length} suits`;
phaseText.style.color = '#ffa726';
}
if (phaseProgress) {
phaseProgress.classList.remove('indeterminate');
}
// Hide stop button
stopButton.style.display = 'none';
// Add log entry
addLogEntry(`Search stopped by user with ${currentSuits.length} suits found`, 'warning');
// Update results count
const countSpan = document.getElementById('resultsCount');
if (countSpan) {
@ -499,10 +784,13 @@ function transformSuitData(suit) {
/**
* Insert a suit into the currentSuits array in score-ordered position (highest first)
* Keeps only top MAX_SUITS to match backend behavior and ensure consistent counts
*/
const MAX_SUITS = 10; // Must match max_results sent to backend
function insertSuitInScoreOrder(suit) {
console.log(`Inserting suit with score ${suit.score}. Current suits:`, currentSuits.map(s => s.score));
// Find the correct position to insert the suit (highest score first)
let insertIndex = 0;
for (let i = 0; i < currentSuits.length; i++) {
@ -512,14 +800,47 @@ function insertSuitInScoreOrder(suit) {
}
insertIndex = i + 1;
}
// If we already have MAX_SUITS and this suit would be at the end (worst), skip it
if (currentSuits.length >= MAX_SUITS && insertIndex >= MAX_SUITS) {
console.log(`Suit with score ${suit.score} not good enough for top ${MAX_SUITS}, skipping`);
return -1; // Signal that suit wasn't inserted
}
// Insert the suit at the correct position
currentSuits.splice(insertIndex, 0, suit);
console.log(`Inserted at index ${insertIndex}. New order:`, currentSuits.map(s => s.score));
return insertIndex;
}
/**
* Trim excess suits from both the array and DOM to maintain MAX_SUITS limit
* This ensures consistent suit counts between runs
*/
function trimExcessSuits() {
const streamingResults = document.getElementById('streamingResults');
if (!streamingResults) return;
// Remove excess suits from array (keep only top MAX_SUITS)
while (currentSuits.length > MAX_SUITS) {
const removed = currentSuits.pop();
console.log(`Trimmed suit with score ${removed.score} (exceeds MAX_SUITS=${MAX_SUITS})`);
}
// Remove excess DOM elements (keep only top MAX_SUITS)
while (streamingResults.children.length > MAX_SUITS) {
const lastChild = streamingResults.lastElementChild;
if (lastChild) {
console.log(`Removing DOM element for suit at position ${streamingResults.children.length}`);
lastChild.remove();
}
}
// Update medals after trimming
updateAllMedals();
}
/**
* Regenerate the entire results display to maintain proper score ordering
*/
@ -1137,30 +1458,107 @@ function selectSuit(suitId) {
* Populate the visual equipment slots with suit items
*/
function populateVisualSlots(items) {
// Clear all slots first
// Clear non-locked 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');
const slotName = slot.id.replace('slot_', '').replace(/_/g, ' ');
// Skip locked slots - preserve their summary
if (!lockedSlots.has(slotName)) {
slot.innerHTML = '<span class="empty-slot">Empty</span>';
slot.parentElement.classList.remove('populated');
}
});
// Populate with items
// Populate non-locked slots with items
Object.entries(items).forEach(([slotName, item]) => {
const slotId = `slot_${slotName.replace(' ', '_')}`;
// Skip locked slots - they keep their configured info
if (lockedSlots.has(slotName)) return;
const slotId = `slot_${slotName.replace(/ /g, '_')}`;
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.source_character || item.character_name || 'Unknown'}</div>
<div class="slot-item-properties">${formatItemProperties(item)}</div>
${needsReducing}
<button class="clear-slot-btn" onclick="clearSlot('${slotName}')" title="Clear slot">x</button>
`;
slotElement.parentElement.classList.add('populated');
}
});
// Update suit summary
updateSuitSummary();
}
/**
* Clear a single slot
*/
function clearSlot(slotName) {
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
const slotContent = document.getElementById(`slot_${slotName.replace(/ /g, '_')}`);
slotContent.innerHTML = '<span class="empty-slot">Empty</span>';
slotElement.classList.remove('populated');
// Also remove from selectedSuit if present
if (selectedSuit && selectedSuit.items[slotName]) {
delete selectedSuit.items[slotName];
}
updateSuitSummary();
}
/**
* Update the suit summary display
*/
function updateSuitSummary() {
const summaryDiv = document.getElementById('suitSummary');
if (!summaryDiv) return;
if (!selectedSuit || Object.keys(selectedSuit.items).length === 0) {
summaryDiv.innerHTML = '<div class="no-summary">Select a suit to see summary</div>';
return;
}
const lines = Object.entries(selectedSuit.items).map(([slot, item]) => {
const spells = (item.spell_names || []).map(s => s.replace('Legendary ', 'L.')).join(', ') || 'No spells';
const char = item.source_character || item.character_name || 'Unknown';
return `<div class="summary-line">
<span class="summary-slot">${slot}:</span>
<span class="summary-item">${item.name}</span>
<span class="summary-spells">[${spells}]</span>
<span class="summary-char">- ${char}</span>
</div>`;
}).join('');
summaryDiv.innerHTML = lines;
}
/**
* Copy suit summary to clipboard
*/
function copySuitSummary() {
if (!selectedSuit) return;
const text = Object.entries(selectedSuit.items).map(([slot, item]) => {
const spells = (item.spell_names || []).join(', ') || 'No spells';
const char = item.source_character || item.character_name || 'Unknown';
return `${slot}: ${item.name} [${spells}] - ${char}`;
}).join('\n');
navigator.clipboard.writeText(text).then(() => {
// Brief visual feedback
const btn = document.querySelector('.copy-summary-btn');
if (btn) {
const original = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = original, 1500);
}
});
}
/**
@ -1169,29 +1567,236 @@ function populateVisualSlots(items) {
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');
// Unlocking - remove from map and clear visual
unlockSlot(slotName);
} else {
lockedSlots.add(slotName);
slotElement.classList.add('locked');
lockBtn.classList.add('locked');
// Locking - check if slot is populated
const isPopulated = slotElement.classList.contains('populated');
if (isPopulated && selectedSuit && selectedSuit.items[slotName]) {
// Auto-extract set/spells from the populated item
const item = selectedSuit.items[slotName];
const lockInfo = {
set: item.set_name || null,
setId: item.set_id || null,
spells: item.spell_names || item.spells || []
};
lockedSlots.set(slotName, lockInfo);
slotElement.classList.add('locked');
lockBtn.classList.add('locked');
renderLockedSlotSummary(slotName, lockInfo);
} else {
// Empty slot - show lock form for manual entry
showLockSlotForm(slotName);
}
}
}
/**
* Unlock a slot and restore its content
*/
function unlockSlot(slotName) {
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
const lockBtn = slotElement.querySelector('.lock-btn');
const slotContent = document.getElementById(`slot_${slotName.replace(/ /g, '_')}`);
lockedSlots.delete(slotName);
slotElement.classList.remove('locked');
lockBtn.classList.remove('locked');
// Restore slot content (either from selected suit or empty)
if (selectedSuit && selectedSuit.items[slotName]) {
const item = selectedSuit.items[slotName];
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
slotContent.innerHTML = `
<div class="slot-item-name">${item.name}</div>
<div class="slot-item-character">${item.source_character || item.character_name || 'Unknown'}</div>
<div class="slot-item-properties">${formatItemProperties(item)}</div>
${needsReducing}
`;
} else {
slotContent.innerHTML = '<span class="empty-slot">Empty</span>';
slotElement.classList.remove('populated');
}
}
/**
* Show lock form for manual slot configuration
*/
function showLockSlotForm(slotName) {
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
const slotContent = document.getElementById(`slot_${slotName.replace(/ /g, '_')}`);
const isArmorSlot = ARMOR_SLOTS.includes(slotName);
// Build spell checkboxes
const spellCheckboxes = [...COMMON_CANTRIPS, ...COMMON_WARDS].map(spell => `
<label class="lock-spell-checkbox">
<input type="checkbox" value="${spell}">
<span>${spell.replace('Legendary ', 'L. ')}</span>
</label>
`).join('');
// Build set dropdown (armor only)
const setDropdown = isArmorSlot ? `
<div class="lock-form-set">
<label>Equipment Set:</label>
<select class="lock-set-select" id="lockSetSelect_${slotName.replace(/ /g, '_')}">
<option value="">None</option>
${getEquipmentSetOptions()}
</select>
</div>
` : '';
slotContent.innerHTML = `
<div class="lock-form">
<div class="lock-form-header">Configure Locked Slot</div>
${setDropdown}
<div class="lock-form-spells">
<label>Spells this item has:</label>
<div class="lock-spell-checkboxes">
${spellCheckboxes}
</div>
</div>
<div class="lock-form-actions">
<button class="lock-save-btn" onclick="saveLockSlotForm('${slotName}')">Lock</button>
<button class="lock-cancel-btn" onclick="cancelLockSlotForm('${slotName}')">Cancel</button>
</div>
</div>
`;
slotElement.classList.add('configuring');
}
/**
* Get equipment set options for dropdown
*/
function getEquipmentSetOptions() {
// Equipment sets - IDs must match getSetNameById() and backend
const commonSets = [
{ id: 14, name: "Adept's" },
{ id: 13, name: "Soldier's" },
{ id: 15, name: "Archer's" },
{ id: 16, name: "Defender's" },
{ id: 19, name: 'Hearty' },
{ id: 20, name: 'Dexterous' },
{ id: 21, name: 'Wise' },
{ id: 22, name: 'Swift' },
{ id: 24, name: 'Reinforced' },
{ id: 26, name: 'Flame Proof' },
{ id: 29, name: 'Lightning Proof' },
{ id: 40, name: 'Heroic Protector' },
{ id: 41, name: 'Heroic Destroyer' },
{ id: 46, name: 'Relic Alduressa' },
{ id: 47, name: 'Ancient Relic' },
{ id: 48, name: 'Noble Relic' }
];
return commonSets.map(set =>
`<option value="${set.id}">${set.name}</option>`
).join('');
}
/**
* Save lock slot form and apply lock
*/
function saveLockSlotForm(slotName) {
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
const lockBtn = slotElement.querySelector('.lock-btn');
const slotContent = document.getElementById(`slot_${slotName.replace(/ /g, '_')}`);
const isArmorSlot = ARMOR_SLOTS.includes(slotName);
// Get selected set (armor only)
let setId = null;
let setName = null;
if (isArmorSlot) {
const setSelect = document.getElementById(`lockSetSelect_${slotName.replace(/ /g, '_')}`);
if (setSelect && setSelect.value) {
setId = parseInt(setSelect.value);
setName = setSelect.options[setSelect.selectedIndex].text;
}
}
// Get selected spells
const selectedSpells = [];
slotContent.querySelectorAll('.lock-spell-checkbox input:checked').forEach(cb => {
selectedSpells.push(cb.value);
});
// Create lock info
const lockInfo = {
set: setName,
setId: setId,
spells: selectedSpells
};
// Store and render
lockedSlots.set(slotName, lockInfo);
slotElement.classList.remove('configuring');
slotElement.classList.add('locked');
lockBtn.classList.add('locked');
renderLockedSlotSummary(slotName, lockInfo);
}
/**
* Cancel lock slot form
*/
function cancelLockSlotForm(slotName) {
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
const slotContent = document.getElementById(`slot_${slotName.replace(/ /g, '_')}`);
slotElement.classList.remove('configuring');
// Restore original content
if (selectedSuit && selectedSuit.items[slotName]) {
const item = selectedSuit.items[slotName];
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
slotContent.innerHTML = `
<div class="slot-item-name">${item.name}</div>
<div class="slot-item-character">${item.source_character || item.character_name || 'Unknown'}</div>
<div class="slot-item-properties">${formatItemProperties(item)}</div>
${needsReducing}
`;
} else {
slotContent.innerHTML = '<span class="empty-slot">Empty</span>';
}
}
/**
* Render locked slot summary display
*/
function renderLockedSlotSummary(slotName, lockInfo) {
const slotContent = document.getElementById(`slot_${slotName.replace(/ /g, '_')}`);
const setBadge = lockInfo.set ?
`<div class="locked-set-badge">${lockInfo.set}</div>` : '';
const spellPills = lockInfo.spells.length > 0 ?
`<div class="locked-spells">
${lockInfo.spells.map(s => `<span class="locked-spell">${s.replace('Legendary ', 'L. ')}</span>`).join('')}
</div>` : '<div class="locked-no-spells">No spells specified</div>';
slotContent.innerHTML = `
<div class="locked-slot-info">
<div class="locked-header">🔒 Locked</div>
${setBadge}
${spellPills}
<button class="unlock-btn" onclick="unlockSlot('${slotName}')">Unlock</button>
</div>
`;
}
/**
* Handle slot click events
*/
function handleSlotClick(slotName) {
// For now, just toggle lock state
// Later this could open item selection dialog
// Could open item selection dialog in the future
console.log(`Clicked slot: ${slotName}`);
}
/**
* Lock currently selected slots
* Lock currently selected/populated slots (auto-extract from items)
*/
function lockSelectedSlots() {
document.querySelectorAll('.slot-item.populated').forEach(slot => {
@ -1206,10 +1811,22 @@ function lockSelectedSlots() {
* Clear all slot locks
*/
function clearAllLocks() {
// Get all locked slot names before clearing
const lockedSlotNames = Array.from(lockedSlots.keys());
lockedSlots.clear();
// Restore each slot
lockedSlotNames.forEach(slotName => {
unlockSlot(slotName);
});
// Also clear any remaining visual lock states
document.querySelectorAll('.slot-item').forEach(slot => {
slot.classList.remove('locked');
slot.querySelector('.lock-btn').classList.remove('locked');
slot.classList.remove('configuring');
const lockBtn = slot.querySelector('.lock-btn');
if (lockBtn) lockBtn.classList.remove('locked');
});
}