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

@ -520,20 +520,7 @@ body {
min-width: 100px; min-width: 100px;
} }
/* Loading States */ /* Loading States - See Progressive Search Styles section below */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-style: italic;
color: #6b7280;
}
.loading::before {
content: "🔍 ";
margin-right: 0.5rem;
}
/* Error States */ /* Error States */
.error { .error {
@ -828,52 +815,718 @@ body {
} }
/* Progressive Search Styles */ /* Progressive Search Styles */
.loading {
padding: 20px;
text-align: center;
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border: 1px solid #90caf9;
border-radius: 8px;
color: #1565c8;
font-weight: 500;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.loading::before {
content: none;
}
/* Spinning search indicator */
.search-spinner {
width: 40px;
height: 40px;
border: 4px solid #bbdefb;
border-top: 4px solid #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.search-status {
font-size: 16px;
font-weight: 600;
color: #1565c8;
}
.search-progress { .search-progress {
margin-top: 15px; margin-top: 8px;
padding: 10px; padding: 12px 16px;
background: #f8f9fa; background: rgba(255, 255, 255, 0.8);
border-radius: 6px; border-radius: 6px;
border: 1px solid #e9ecef; border: 1px solid #90caf9;
width: 100%;
max-width: 400px;
} }
.progress-stats { .progress-stats {
font-family: 'Courier New', monospace; display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.stat-item {
font-size: 13px; font-size: 13px;
color: #495057; color: #424242;
margin-bottom: 10px; }
.stat-value {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #1565c8;
} }
.stop-search-btn { .stop-search-btn {
background: #dc3545; background: linear-gradient(135deg, #ef5350 0%, #d32f2f 100%);
color: white; color: white;
border: none; border: none;
padding: 6px 12px; padding: 8px 16px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 13px;
font-weight: bold; font-weight: bold;
transition: background-color 0.2s; transition: all 0.2s;
box-shadow: 0 2px 4px rgba(211, 47, 47, 0.3);
} }
.stop-search-btn:hover { .stop-search-btn:hover {
background: #c82333; background: linear-gradient(135deg, #d32f2f 0%, #c62828 100%);
transform: translateY(-1px);
box-shadow: 0 3px 6px rgba(211, 47, 47, 0.4);
} }
.stop-search-btn:active { .stop-search-btn:active {
background: #bd2130; transform: translateY(0);
box-shadow: 0 1px 2px rgba(211, 47, 47, 0.3);
}
.search-complete {
font-size: 15px;
color: #2e7d32;
font-weight: 600;
padding: 8px;
} }
#streamingResults { #streamingResults {
margin-top: 15px; margin-top: 15px;
} }
/* Enhance loading messages */ /* ========================================
.loading { ENHANCED SEARCH STATUS UI
padding: 20px; ======================================== */
/* Search Status Container - Modern Dark Design */
.search-status-container {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 24px;
color: #eee;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
}
/* Phase Indicator */
.phase-indicator {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.phase-icon {
font-size: 32px;
animation: pulse 1.5s infinite;
min-width: 40px;
text-align: center; text-align: center;
background: #e3f2fd; }
border: 1px solid #bbdefb;
.phase-icon.complete {
animation: none;
}
.phase-icon.searching {
animation: searchPulse 0.8s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
}
@keyframes searchPulse {
0%, 100% { transform: scale(1) rotate(0deg); }
25% { transform: scale(1.1) rotate(-5deg); }
75% { transform: scale(1.1) rotate(5deg); }
}
.phase-text {
font-size: 18px;
font-weight: 600;
color: #4fc3f7;
}
.phase-text.complete {
color: #66bb6a;
}
/* Progress Bars */
.progress-bars {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.progress-bar-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-bar-container label {
font-size: 11px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
}
.progress-bar {
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.phase-progress {
background: linear-gradient(90deg, #4fc3f7, #29b6f6);
}
.bucket-progress {
background: linear-gradient(90deg, #66bb6a, #43a047);
}
.progress-fill.indeterminate {
width: 30% !important;
animation: indeterminate 1.5s infinite linear;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.progress-label {
font-size: 11px;
color: #aaa;
margin-top: 2px;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 16px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.2s, border-color 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
border-color: rgba(79, 195, 247, 0.3);
}
.stat-card.highlight {
background: rgba(79, 195, 247, 0.15);
border-color: rgba(79, 195, 247, 0.3);
}
.stat-label {
font-size: 11px;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
}
.search-status-container .stat-value {
font-size: 28px;
font-weight: 700;
color: #fff;
font-family: 'Courier New', monospace;
}
.stat-card.highlight .stat-value {
color: #4fc3f7;
}
.stat-unit {
font-size: 10px;
color: #666;
}
/* Verbose Log */
.verbose-log-section {
margin-bottom: 16px;
}
.verbose-log-section summary {
cursor: pointer;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px; border-radius: 6px;
color: #1976d2; font-size: 13px;
color: #aaa;
display: flex;
align-items: center;
gap: 8px;
user-select: none;
}
.verbose-log-section summary:hover {
background: rgba(255, 255, 255, 0.08);
}
.verbose-log-section summary::marker {
color: #4fc3f7;
}
.log-count {
background: rgba(79, 195, 247, 0.2);
color: #4fc3f7;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
margin-left: auto;
}
.verbose-log {
max-height: 200px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.3);
border-radius: 6px;
padding: 8px;
margin-top: 8px;
font-family: 'Courier New', monospace;
font-size: 11px;
}
.log-entry {
padding: 2px 4px;
border-radius: 2px;
margin-bottom: 2px;
}
.log-entry .log-time {
color: #666;
margin-right: 8px;
}
.log-info .log-message { color: #aaa; }
.log-debug .log-message { color: #888; }
.log-success .log-message { color: #66bb6a; }
.log-warning .log-message { color: #ffa726; }
.log-phase { background: rgba(79, 195, 247, 0.1); }
.log-phase .log-message { color: #4fc3f7; }
/* Enhanced Stop Button for new container */
.search-status-container .stop-search-btn {
width: 100%;
padding: 12px 24px;
background: linear-gradient(135deg, #e53935, #c62828);
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(198, 40, 40, 0.3);
}
.search-status-container .stop-search-btn:hover {
background: linear-gradient(135deg, #c62828, #b71c1c);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(198, 40, 40, 0.4);
}
.search-status-container .stop-search-btn:active {
transform: translateY(0);
}
.stop-icon {
font-size: 16px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.search-status-container {
padding: 16px;
}
.phase-icon {
font-size: 24px;
}
.phase-text {
font-size: 14px;
}
}
/* ========================================
SLOT LOCK FORM & LOCKED DISPLAY
======================================== */
/* Slot configuring state */
.slot-item.configuring {
border-color: #4fc3f7 !important;
background: rgba(79, 195, 247, 0.1) !important;
}
/* Lock Form */
.lock-form {
padding: 8px;
font-size: 12px;
}
.lock-form-header {
font-weight: 600;
color: #333;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid #ddd;
}
.lock-form-set {
margin-bottom: 10px;
}
.lock-form-set label {
display: block;
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.lock-set-select {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 12px;
background: white;
}
.lock-form-spells {
margin-bottom: 10px;
}
.lock-form-spells > label {
display: block;
font-size: 11px;
color: #666;
margin-bottom: 6px;
}
.lock-spell-checkboxes {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
max-height: 150px;
overflow-y: auto;
padding: 4px;
background: #f8f8f8;
border-radius: 4px;
}
.lock-spell-checkbox {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 2px 4px;
border-radius: 3px;
cursor: pointer;
}
.lock-spell-checkbox:hover {
background: rgba(0, 0, 0, 0.05);
}
.lock-spell-checkbox input {
margin: 0;
width: 12px;
height: 12px;
}
.lock-spell-checkbox span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lock-form-actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
.lock-save-btn,
.lock-cancel-btn {
flex: 1;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.lock-save-btn {
background: linear-gradient(135deg, #e53935, #c62828);
color: white;
}
.lock-save-btn:hover {
background: linear-gradient(135deg, #c62828, #b71c1c);
}
.lock-cancel-btn {
background: #e0e0e0;
color: #333;
}
.lock-cancel-btn:hover {
background: #d0d0d0;
}
/* Locked Slot Display */
.locked-slot-info {
padding: 8px;
text-align: center;
}
.locked-header {
font-weight: 600;
color: #c62828;
margin-bottom: 8px;
font-size: 13px;
}
.locked-set-badge {
display: inline-block;
background: linear-gradient(135deg, #7b1fa2, #512da8);
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
margin-bottom: 8px;
}
.locked-spells {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
margin-bottom: 8px;
}
.locked-spell {
display: inline-block;
background: rgba(79, 195, 247, 0.2);
color: #0277bd;
padding: 2px 8px;
border-radius: 10px;
font-size: 9px;
font-weight: 500; font-weight: 500;
} }
.locked-no-spells {
font-size: 10px;
color: #999;
font-style: italic;
margin-bottom: 8px;
}
.unlock-btn {
padding: 4px 12px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 11px;
color: #666;
cursor: pointer;
transition: all 0.2s;
}
.unlock-btn:hover {
background: #e8e8e8;
border-color: #ccc;
color: #333;
}
/* Override locked slot styling for better visibility */
.slot-item.locked {
border-color: #c62828 !important;
background: linear-gradient(135deg, rgba(198, 40, 40, 0.1), rgba(198, 40, 40, 0.05)) !important;
}
.slot-item.locked .slot-header {
color: #c62828;
}
.slot-item.locked .lock-btn {
color: #c62828 !important;
opacity: 1 !important;
}
/* ========================================
CLEAR SLOT BUTTON
======================================== */
.slot-content {
position: relative;
}
.clear-slot-btn {
position: absolute;
top: 2px;
right: 2px;
background: rgba(220, 38, 38, 0.1);
border: 1px solid rgba(220, 38, 38, 0.3);
border-radius: 3px;
cursor: pointer;
font-size: 10px;
color: #dc2626;
opacity: 0.4;
padding: 1px 4px;
transition: all 0.2s;
}
.clear-slot-btn:hover {
opacity: 1;
background: rgba(220, 38, 38, 0.2);
}
/* ========================================
SUIT SUMMARY SECTION
======================================== */
.suit-summary-section {
margin-top: 15px;
padding: 12px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 8px;
border: 1px solid #2d3748;
}
.summary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #2d3748;
}
.summary-header h4 {
color: #4fc3f7;
font-size: 14px;
margin: 0;
}
.copy-summary-btn {
background: rgba(79, 195, 247, 0.2);
border: 1px solid rgba(79, 195, 247, 0.3);
color: #4fc3f7;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.copy-summary-btn:hover {
background: rgba(79, 195, 247, 0.3);
}
.suit-summary-content {
font-family: 'Courier New', monospace;
font-size: 11px;
max-height: 300px;
overflow-y: auto;
}
.no-summary {
color: #666;
font-style: italic;
text-align: center;
padding: 10px;
}
.summary-line {
padding: 4px 0;
border-bottom: 1px solid #2d3748;
color: #ccc;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.summary-line:last-child {
border-bottom: none;
}
.summary-slot {
color: #888;
min-width: 85px;
font-weight: 600;
}
.summary-item {
color: #4ecdc4;
font-weight: 600;
flex: 1;
min-width: 150px;
}
.summary-spells {
color: #ffe66d;
font-size: 10px;
}
.summary-char {
color: #95a5a6;
font-size: 10px;
margin-left: auto;
}

View file

@ -162,53 +162,53 @@
</div> </div>
<!-- Legendary Weapon Skills --> <!-- Legendary Weapon Skills -->
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_finesse" value="Legendary Finesse Weapons"> <input type="checkbox" id="cantrip_legendary_finesse" value="Legendary Finesse Weapon Aptitude">
<label for="cantrip_legendary_finesse">Finesse</label> <label for="cantrip_legendary_finesse">Finesse</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_heavy" value="Legendary Heavy Weapons"> <input type="checkbox" id="cantrip_legendary_heavy" value="Legendary Heavy Weapon Aptitude">
<label for="cantrip_legendary_heavy">Heavy</label> <label for="cantrip_legendary_heavy">Heavy</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_light" value="Legendary Light Weapons"> <input type="checkbox" id="cantrip_legendary_light" value="Legendary Light Weapon Aptitude">
<label for="cantrip_legendary_light">Light</label> <label for="cantrip_legendary_light">Light</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_missile" value="Legendary Missile Weapons"> <input type="checkbox" id="cantrip_legendary_missile" value="Legendary Missile Weapon Aptitude">
<label for="cantrip_legendary_missile">Missile</label> <label for="cantrip_legendary_missile">Missile</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_twohanded" value="Legendary Two Handed Combat"> <input type="checkbox" id="cantrip_legendary_twohanded" value="Legendary Two Handed Combat Aptitude">
<label for="cantrip_legendary_twohanded">Two-handed</label> <label for="cantrip_legendary_twohanded">Two-handed</label>
</div> </div>
<!-- Legendary Magic Skills --> <!-- Legendary Magic Skills -->
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_war" value="Legendary War Magic"> <input type="checkbox" id="cantrip_legendary_war" value="Legendary War Magic Aptitude">
<label for="cantrip_legendary_war">War Magic</label> <label for="cantrip_legendary_war">War Magic</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_void" value="Legendary Void Magic"> <input type="checkbox" id="cantrip_legendary_void" value="Legendary Void Magic Aptitude">
<label for="cantrip_legendary_void">Void Magic</label> <label for="cantrip_legendary_void">Void Magic</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_creature" value="Legendary Creature Enchantment"> <input type="checkbox" id="cantrip_legendary_creature" value="Legendary Creature Enchantment Aptitude">
<label for="cantrip_legendary_creature">Creature</label> <label for="cantrip_legendary_creature">Creature</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_item" value="Legendary Item Enchantment"> <input type="checkbox" id="cantrip_legendary_item" value="Legendary Item Enchantment Aptitude">
<label for="cantrip_legendary_item">Item</label> <label for="cantrip_legendary_item">Item</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_life" value="Legendary Life Magic"> <input type="checkbox" id="cantrip_legendary_life" value="Legendary Life Magic Aptitude">
<label for="cantrip_legendary_life">Life Magic</label> <label for="cantrip_legendary_life">Life Magic</label>
</div> </div>
<!-- Legendary Defense --> <!-- Legendary Defense -->
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_magic_defense" value="Legendary Magic Defense"> <input type="checkbox" id="cantrip_legendary_magic_defense" value="Legendary Magic Resistance">
<label for="cantrip_legendary_magic_defense">Magic Def</label> <label for="cantrip_legendary_magic_defense">Magic Def</label>
</div> </div>
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_melee_defense" value="Legendary Melee Defense"> <input type="checkbox" id="cantrip_legendary_melee_defense" value="Legendary Invulnerability">
<label for="cantrip_legendary_melee_defense">Melee Def</label> <label for="cantrip_legendary_melee_defense">Melee Def</label>
</div> </div>
</div> </div>
@ -458,6 +458,17 @@
<button type="button" class="btn btn-secondary" id="clearAllLocks">Clear All Locks</button> <button type="button" class="btn btn-secondary" id="clearAllLocks">Clear All Locks</button>
<button type="button" class="btn btn-secondary" id="resetSlotView">Reset View</button> <button type="button" class="btn btn-secondary" id="resetSlotView">Reset View</button>
</div> </div>
<!-- Suit Summary -->
<div class="suit-summary-section">
<div class="summary-header">
<h4>Suit Summary</h4>
<button class="copy-summary-btn" onclick="copySuitSummary()" title="Copy to clipboard">Copy</button>
</div>
<div id="suitSummary" class="suit-summary-content">
<div class="no-summary">Select a suit to see summary</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,13 +1,54 @@
// Suitbuilder JavaScript - Constraint Solver Frontend Logic // 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 // Configuration
const API_BASE = '/inv/suitbuilder'; const API_BASE = '/inv/suitbuilder';
let currentSuits = []; let currentSuits = [];
let lockedSlots = new Set(); let lockedSlots = new Map(); // slot -> { set: string|null, setId: number|null, spells: string[] }
let selectedSuit = null; let selectedSuit = null;
let currentSearchController = null; // AbortController for current search 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 // Initialize when page loads
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initializeSuitbuilder(); initializeSuitbuilder();
@ -134,6 +175,10 @@ function setupSlotInteractions() {
/** /**
* Perform suit search with current constraints using streaming results * Perform suit search with current constraints using streaming results
*/ */
// Global timer for search
let searchStartTime = null;
let searchTimerInterval = null;
async function performSuitSearch() { async function performSuitSearch() {
const constraints = gatherConstraints(); const constraints = gatherConstraints();
@ -144,24 +189,83 @@ async function performSuitSearch() {
const resultsDiv = document.getElementById('suitResults'); const resultsDiv = document.getElementById('suitResults');
const countSpan = document.getElementById('resultsCount'); 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 = []; currentSuits = [];
resultsDiv.innerHTML = ` resultsDiv.innerHTML = `
<div class="loading"> <div class="search-status-container">
🔍 Searching for optimal suits... <!-- Phase indicator with animated icon -->
<div class="search-progress"> <div class="phase-indicator">
<div class="progress-stats"> <div class="phase-icon" id="phaseIcon"></div>
Found: <span id="foundCount">0</span> suits | <div class="phase-text" id="searchPhase">Initializing...</div>
Checked: <span id="checkedCount">0</span> combinations |
Time: <span id="elapsedTime">0.0</span>s
</div> </div>
<button id="stopSearch" class="stop-search-btn">Stop Search</button>
<!-- 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> </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> <div id="streamingResults"></div>
`; `;
countSpan.textContent = ''; 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 { try {
await streamOptimalSuits(constraints); await streamOptimalSuits(constraints);
} catch (error) { } catch (error) {
@ -173,6 +277,12 @@ async function performSuitSearch() {
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`; resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
countSpan.textContent = ''; countSpan.textContent = '';
} }
} finally {
// Stop client-side timer
if (searchTimerInterval) {
clearInterval(searchTimerInterval);
searchTimerInterval = null;
}
} }
} }
@ -212,8 +322,16 @@ function gatherConstraints() {
.filter(cb => cb.checked) .filter(cb => cb.checked)
.map(cb => cb.value), .map(cb => cb.value),
// Locked slots // Locked slots - convert Map to object for API
locked_slots: Array.from(lockedSlots) locked_slots: Object.fromEntries(
Array.from(lockedSlots.entries()).map(([slot, info]) => [
slot,
{
set_id: info.setId || null,
spells: info.spells || []
}
])
)
}; };
return constraints; return constraints;
@ -252,7 +370,7 @@ async function streamOptimalSuits(constraints) {
...constraints.legendary_cantrips, ...constraints.legendary_cantrips,
...constraints.protection_spells ...constraints.protection_spells
], ],
locked_items: {}, // TODO: implement locked items locked_slots: constraints.locked_slots || {},
include_equipped: constraints.include_equipped, include_equipped: constraints.include_equipped,
include_inventory: constraints.include_inventory, include_inventory: constraints.include_inventory,
min_armor: constraints.min_armor ? parseInt(constraints.min_armor) : null, min_armor: constraints.min_armor ? parseInt(constraints.min_armor) : null,
@ -336,6 +454,10 @@ async function streamOptimalSuits(constraints) {
handleSuitEvent(eventData); handleSuitEvent(eventData);
} else if (currentEventType === 'progress') { } else if (currentEventType === 'progress') {
handleProgressEvent(eventData); handleProgressEvent(eventData);
} else if (currentEventType === 'phase') {
handlePhaseEvent(eventData);
} else if (currentEventType === 'log') {
handleLogEvent(eventData);
} else if (currentEventType === 'complete') { } else if (currentEventType === 'complete') {
handleCompleteEvent(eventData); handleCompleteEvent(eventData);
resolve(); resolve();
@ -381,10 +503,19 @@ async function streamOptimalSuits(constraints) {
const insertIndex = insertSuitInScoreOrder(transformedSuit); const insertIndex = insertSuitInScoreOrder(transformedSuit);
console.log('Insert index returned:', insertIndex); 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); insertSuitDOMAtPosition(transformedSuit, insertIndex);
console.log('DOM insertion complete'); console.log('DOM insertion complete');
// Trim excess suits from array and DOM to maintain consistent count
trimExcessSuits();
// Update count // Update count
document.getElementById('foundCount').textContent = currentSuits.length; document.getElementById('foundCount').textContent = currentSuits.length;
document.getElementById('resultsCount').textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`; document.getElementById('resultsCount').textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
@ -397,21 +528,158 @@ async function streamOptimalSuits(constraints) {
function handleProgressEvent(data) { function handleProgressEvent(data) {
try { try {
document.getElementById('foundCount').textContent = data.found || currentSuits.length; // Always show frontend's actual suit count for consistency
document.getElementById('checkedCount').textContent = data.evaluated || 0; document.getElementById('foundCount').textContent = currentSuits.length;
document.getElementById('elapsedTime').textContent = data.elapsed || '0.0'; 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) { } catch (error) {
console.error('Error processing progress data:', 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) { function handleCompleteEvent(data) {
try { try {
// Hide loading indicator const duration = ((Date.now() - searchStartTime) / 1000).toFixed(1);
const loadingDiv = document.querySelector('.loading');
if (loadingDiv) { // Update phase indicator to complete
loadingDiv.innerHTML = `✅ Search complete! Found ${data.suits_found} suits in ${data.duration}s.`; 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 // Update final results count
const countSpan = document.getElementById('resultsCount'); const countSpan = document.getElementById('resultsCount');
@ -446,11 +714,28 @@ async function streamOptimalSuits(constraints) {
currentSearchController = null; currentSearchController = null;
} }
// Update UI to show search was stopped // Update phase indicator to show stopped
const loadingDiv = document.querySelector('.loading'); const phaseIcon = document.getElementById('phaseIcon');
if (loadingDiv) { const phaseText = document.getElementById('searchPhase');
loadingDiv.innerHTML = '⏹️ Search stopped by user.'; 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 // Update results count
const countSpan = document.getElementById('resultsCount'); const countSpan = document.getElementById('resultsCount');
@ -499,7 +784,10 @@ function transformSuitData(suit) {
/** /**
* Insert a suit into the currentSuits array in score-ordered position (highest first) * 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) { function insertSuitInScoreOrder(suit) {
console.log(`Inserting suit with score ${suit.score}. Current suits:`, currentSuits.map(s => s.score)); console.log(`Inserting suit with score ${suit.score}. Current suits:`, currentSuits.map(s => s.score));
@ -513,6 +801,12 @@ function insertSuitInScoreOrder(suit) {
insertIndex = i + 1; 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 // Insert the suit at the correct position
currentSuits.splice(insertIndex, 0, suit); currentSuits.splice(insertIndex, 0, suit);
@ -520,6 +814,33 @@ function insertSuitInScoreOrder(suit) {
return insertIndex; 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 * Regenerate the entire results display to maintain proper score ordering
*/ */
@ -1137,16 +1458,22 @@ function selectSuit(suitId) {
* Populate the visual equipment slots with suit items * Populate the visual equipment slots with suit items
*/ */
function populateVisualSlots(items) { function populateVisualSlots(items) {
// Clear all slots first // Clear non-locked slots first
document.querySelectorAll('.slot-content').forEach(slot => { document.querySelectorAll('.slot-content').forEach(slot => {
const slotName = slot.id.replace('slot_', '').replace('_', ' '); 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.innerHTML = '<span class="empty-slot">Empty</span>';
slot.parentElement.classList.remove('populated'); slot.parentElement.classList.remove('populated');
}
}); });
// Populate with items // Populate non-locked slots with items
Object.entries(items).forEach(([slotName, item]) => { 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); const slotElement = document.getElementById(slotId);
if (slotElement) { if (slotElement) {
@ -1157,10 +1484,81 @@ function populateVisualSlots(items) {
<div class="slot-item-character">${item.source_character || item.character_name || 'Unknown'}</div> <div class="slot-item-character">${item.source_character || item.character_name || 'Unknown'}</div>
<div class="slot-item-properties">${formatItemProperties(item)}</div> <div class="slot-item-properties">${formatItemProperties(item)}</div>
${needsReducing} ${needsReducing}
<button class="clear-slot-btn" onclick="clearSlot('${slotName}')" title="Clear slot">x</button>
`; `;
slotElement.parentElement.classList.add('populated'); 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);
}
});
} }
/** /**
@ -1171,27 +1569,234 @@ function toggleSlotLock(slotName) {
const lockBtn = slotElement.querySelector('.lock-btn'); const lockBtn = slotElement.querySelector('.lock-btn');
if (lockedSlots.has(slotName)) { if (lockedSlots.has(slotName)) {
// Unlocking - remove from map and clear visual
unlockSlot(slotName);
} else {
// 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); lockedSlots.delete(slotName);
slotElement.classList.remove('locked'); slotElement.classList.remove('locked');
lockBtn.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 { } else {
lockedSlots.add(slotName); 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'); slotElement.classList.add('locked');
lockBtn.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 * Handle slot click events
*/ */
function handleSlotClick(slotName) { function handleSlotClick(slotName) {
// For now, just toggle lock state // Could open item selection dialog in the future
// Later this could open item selection dialog
console.log(`Clicked slot: ${slotName}`); console.log(`Clicked slot: ${slotName}`);
} }
/** /**
* Lock currently selected slots * Lock currently selected/populated slots (auto-extract from items)
*/ */
function lockSelectedSlots() { function lockSelectedSlots() {
document.querySelectorAll('.slot-item.populated').forEach(slot => { document.querySelectorAll('.slot-item.populated').forEach(slot => {
@ -1206,10 +1811,22 @@ function lockSelectedSlots() {
* Clear all slot locks * Clear all slot locks
*/ */
function clearAllLocks() { function clearAllLocks() {
// Get all locked slot names before clearing
const lockedSlotNames = Array.from(lockedSlots.keys());
lockedSlots.clear(); lockedSlots.clear();
// Restore each slot
lockedSlotNames.forEach(slotName => {
unlockSlot(slotName);
});
// Also clear any remaining visual lock states
document.querySelectorAll('.slot-item').forEach(slot => { document.querySelectorAll('.slot-item').forEach(slot => {
slot.classList.remove('locked'); 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');
}); });
} }