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:
parent
0fd539bedf
commit
8e70f88de1
3 changed files with 1391 additions and 110 deletions
|
|
@ -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;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,34 +175,97 @@ 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();
|
||||||
|
|
||||||
if (!validateConstraints(constraints)) {
|
if (!validateConstraints(constraints)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
|
||||||
<button id="stopSearch" class="stop-search-btn">Stop Search</button>
|
|
||||||
</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>
|
||||||
<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,10 +322,18 @@ 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();
|
||||||
|
|
@ -372,23 +494,32 @@ async function streamOptimalSuits(constraints) {
|
||||||
function handleSuitEvent(data) {
|
function handleSuitEvent(data) {
|
||||||
try {
|
try {
|
||||||
console.log('NEW handleSuitEvent called with data:', data);
|
console.log('NEW handleSuitEvent called with data:', data);
|
||||||
|
|
||||||
// Transform backend suit format to frontend format
|
// Transform backend suit format to frontend format
|
||||||
const transformedSuit = transformSuitData(data);
|
const transformedSuit = transformSuitData(data);
|
||||||
console.log('Transformed suit:', transformedSuit);
|
console.log('Transformed suit:', transformedSuit);
|
||||||
|
|
||||||
// Insert suit in score-ordered position (highest score first)
|
// Insert suit in score-ordered position (highest score first)
|
||||||
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' : ''}`;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing suit data:', error);
|
console.error('Error processing suit data:', error);
|
||||||
console.error('Stack trace:', error.stack);
|
console.error('Stack trace:', error.stack);
|
||||||
|
|
@ -397,28 +528,165 @@ 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');
|
||||||
if (countSpan) {
|
if (countSpan) {
|
||||||
countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
|
countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing completion data:', error);
|
console.error('Error processing completion data:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -439,19 +707,36 @@ async function streamOptimalSuits(constraints) {
|
||||||
const stopButton = document.getElementById('stopSearch');
|
const stopButton = document.getElementById('stopSearch');
|
||||||
stopButton.addEventListener('click', () => {
|
stopButton.addEventListener('click', () => {
|
||||||
searchStopped = true;
|
searchStopped = true;
|
||||||
|
|
||||||
// Actually abort the HTTP request
|
// Actually abort the HTTP request
|
||||||
if (currentSearchController) {
|
if (currentSearchController) {
|
||||||
currentSearchController.abort();
|
currentSearchController.abort();
|
||||||
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');
|
||||||
if (countSpan) {
|
if (countSpan) {
|
||||||
|
|
@ -499,10 +784,13 @@ 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));
|
||||||
|
|
||||||
// Find the correct position to insert the suit (highest score first)
|
// Find the correct position to insert the suit (highest score first)
|
||||||
let insertIndex = 0;
|
let insertIndex = 0;
|
||||||
for (let i = 0; i < currentSuits.length; i++) {
|
for (let i = 0; i < currentSuits.length; i++) {
|
||||||
|
|
@ -512,14 +800,47 @@ 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);
|
||||||
|
|
||||||
console.log(`Inserted at index ${insertIndex}. New order:`, currentSuits.map(s => s.score));
|
console.log(`Inserted at index ${insertIndex}. New order:`, currentSuits.map(s => s.score));
|
||||||
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,30 +1458,107 @@ 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, ' ');
|
||||||
slot.innerHTML = '<span class="empty-slot">Empty</span>';
|
// Skip locked slots - preserve their summary
|
||||||
slot.parentElement.classList.remove('populated');
|
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]) => {
|
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) {
|
||||||
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
|
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
|
||||||
|
|
||||||
slotElement.innerHTML = `
|
slotElement.innerHTML = `
|
||||||
<div class="slot-item-name">${item.name}</div>
|
<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-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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1169,29 +1567,236 @@ function populateVisualSlots(items) {
|
||||||
function toggleSlotLock(slotName) {
|
function toggleSlotLock(slotName) {
|
||||||
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
|
const slotElement = document.querySelector(`[data-slot="${slotName}"]`);
|
||||||
const lockBtn = slotElement.querySelector('.lock-btn');
|
const lockBtn = slotElement.querySelector('.lock-btn');
|
||||||
|
|
||||||
if (lockedSlots.has(slotName)) {
|
if (lockedSlots.has(slotName)) {
|
||||||
lockedSlots.delete(slotName);
|
// Unlocking - remove from map and clear visual
|
||||||
slotElement.classList.remove('locked');
|
unlockSlot(slotName);
|
||||||
lockBtn.classList.remove('locked');
|
|
||||||
} else {
|
} else {
|
||||||
lockedSlots.add(slotName);
|
// Locking - check if slot is populated
|
||||||
slotElement.classList.add('locked');
|
const isPopulated = slotElement.classList.contains('populated');
|
||||||
lockBtn.classList.add('locked');
|
|
||||||
|
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
|
* 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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue