reduced duplicate insert errors of portals, still present because of two players disovering the same portal at the same time, other changes to inventory

This commit is contained in:
erik 2025-09-22 18:21:04 +00:00
parent e7ca39318f
commit 6c646719dd
6 changed files with 1093 additions and 232 deletions

View file

@ -90,53 +90,89 @@
border: 1px solid #ccc;
}
/* Filter Section Styling */
.filter-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 8px;
margin-bottom: 8px;
}
.filter-card-header {
font-weight: bold;
font-size: 11px;
color: #495057;
margin-bottom: 6px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 2px;
}
.filter-row {
display: flex;
gap: 10px;
margin-bottom: 5px;
gap: 8px;
margin-bottom: 6px;
align-items: center;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 3px;
gap: 4px;
min-width: 0;
}
.filter-group label {
font-weight: bold;
font-size: 10px;
color: #000;
font-weight: 600;
font-size: 11px;
color: #343a40;
min-width: 60px;
text-align: right;
}
.filter-group-wide label {
min-width: 80px;
}
input[type="text"],
input[type="number"],
select {
border: 1px solid #999;
padding: 1px 3px;
border: 1px solid #ced4da;
border-radius: 3px;
padding: 4px 6px;
font-size: 11px;
height: 18px;
height: 24px;
background: white;
}
input[type="text"]:focus,
input[type="number"]:focus,
select:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
input[type="text"] {
width: 120px;
width: 140px;
}
input[type="number"] {
width: 40px;
width: 50px;
}
select {
width: 100px;
width: 110px;
}
.filter-section {
display: flex;
align-items: flex-start;
gap: 5px;
margin-bottom: 3px;
.range-separator {
color: #6c757d;
font-weight: bold;
margin: 0 4px;
}
.section-label {
font-weight: bold;
font-size: 10px;
@ -145,11 +181,18 @@
color: #000;
}
.checkbox-sections-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.checkbox-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
flex: 1;
gap: 4px;
max-height: 150px;
overflow-y: auto;
}
.checkbox-item {
@ -157,6 +200,8 @@
align-items: center;
font-size: 9px;
white-space: nowrap;
width: calc(50% - 2px);
min-width: 80px;
}
.checkbox-item input[type="checkbox"] {
@ -180,8 +225,9 @@
.search-actions {
display: flex;
gap: 5px;
margin-top: 3px;
gap: 10px;
margin-top: 15px;
justify-content: flex-start;
}
.btn {
@ -429,32 +475,52 @@
<div class="main-content">
<form class="search-form" id="inventorySearchForm">
<!-- Row 0: Equipment Type Selection -->
<div class="filter-row">
<div class="filter-group">
<label>Type:</label>
<div style="display: flex; gap: 10px;">
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="armorOnly" value="armor" checked style="margin-right: 3px;">
Armor Only
</label>
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="jewelryOnly" value="jewelry" style="margin-right: 3px;">
Jewelry Only
</label>
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="allItems" value="all" style="margin-right: 3px;">
All Items
</label>
</div>
</div>
</div>
<!-- Row 0.5: Slot Selection -->
<div class="filter-row">
<div class="filter-group">
<label>Slot:</label>
<select id="slotFilter">
<!-- Basic Filters -->
<div class="filter-card">
<div class="filter-card-header">Basic Search</div>
<div class="filter-row">
<div class="filter-group">
<label>Name:</label>
<input type="text" id="searchText" placeholder="Item name">
</div>
<div class="filter-group">
<label>Status:</label>
<select id="searchEquipStatus">
<option value="all">All</option>
<option value="equipped">Equipped</option>
<option value="unequipped">Inventory</option>
</select>
</div>
<div class="filter-group">
<label>Type:</label>
<div style="display: flex; gap: 8px;">
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="armorOnly" value="armor" checked style="margin-right: 3px;">
Armor Only
</label>
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="jewelryOnly" value="jewelry" style="margin-right: 3px;">
Jewelry Only
</label>
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="shirtOnly" value="shirt" style="margin-right: 3px;">
Shirts Only
</label>
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="pantsOnly" value="pants" style="margin-right: 3px;">
Pants Only
</label>
<label style="display: flex; align-items: center; font-weight: normal;">
<input type="radio" name="equipmentType" id="allItems" value="all" style="margin-right: 3px;">
All Items
</label>
</div>
</div>
<div class="filter-group">
<label>Slot:</label>
<select id="slotFilter">
<option value="">All Slots</option>
<optgroup label="Armor Slots">
<option value="Head">Head</option>
@ -480,66 +546,61 @@
</div>
</div>
<!-- Row 1: Basic filters -->
<div class="filter-row">
<div class="filter-group">
<label>Name:</label>
<input type="text" id="searchText" placeholder="Item name">
<!-- Stats Filters -->
<div class="filter-card">
<div class="filter-card-header">Item Stats</div>
<div class="filter-row">
<div class="filter-group">
<label>Armor:</label>
<input type="number" id="searchMinArmor" placeholder="Min">
<span class="range-separator">-</span>
<input type="number" id="searchMaxArmor" placeholder="Max">
</div>
<div class="filter-group">
<label>Crit Dmg:</label>
<input type="number" id="searchMinCritDamage" placeholder="Min">
<span class="range-separator">-</span>
<input type="number" id="searchMaxCritDamage" placeholder="Max">
</div>
</div>
<div class="filter-group">
<label>Status:</label>
<select id="searchEquipStatus">
<option value="all">All</option>
<option value="equipped">Equipped</option>
<option value="unequipped">Inventory</option>
</select>
<div class="filter-row">
<div class="filter-group">
<label>Dmg Rating:</label>
<input type="number" id="searchMinDamageRating" placeholder="Min">
<span class="range-separator">-</span>
<input type="number" id="searchMaxDamageRating" placeholder="Max">
</div>
<div class="filter-group">
<label>Heal Boost:</label>
<input type="number" id="searchMinHealBoost" placeholder="Min">
<span class="range-separator">-</span>
<input type="number" id="searchMaxHealBoost" placeholder="Max">
</div>
</div>
</div>
<!-- Row 2: Stats -->
<div class="filter-row">
<div class="filter-group">
<label>Armor:</label>
<input type="number" id="searchMinArmor" placeholder="Min">
<span>-</span>
<input type="number" id="searchMaxArmor" placeholder="Max">
</div>
<div class="filter-group">
<label>Crit:</label>
<input type="number" id="searchMinCritDamage" placeholder="Min">
<span>-</span>
<input type="number" id="searchMaxCritDamage" placeholder="Max">
</div>
<div class="filter-group">
<label>Dmg:</label>
<input type="number" id="searchMinDamageRating" placeholder="Min">
<span>-</span>
<input type="number" id="searchMaxDamageRating" placeholder="Max">
</div>
<div class="filter-group">
<label>Heal:</label>
<input type="number" id="searchMinHealBoost" placeholder="Min">
<span>-</span>
<input type="number" id="searchMaxHealBoost" placeholder="Max">
</div>
</div>
<!-- New Rating Filters -->
<div class="filter-row">
<div class="filter-group">
<label>Vitality:</label>
<input type="number" id="searchMinVitalityRating" placeholder="Min">
</div>
<div class="filter-group">
<label>Dmg Resist:</label>
<input type="number" id="searchMinDamageResistRating" placeholder="Min">
<div class="filter-row">
<div class="filter-group">
<label>Vitality:</label>
<input type="number" id="searchMinVitalityRating" placeholder="Min">
</div>
<div class="filter-group">
<label>Dmg Resist:</label>
<input type="number" id="searchMinDamageResistRating" placeholder="Min">
</div>
<div class="filter-group">
<label>Crit Dmg Resist:</label>
<input type="number" id="searchMinCritDamageResistRating" placeholder="Min">
</div>
</div>
</div>
<!-- Equipment Sets -->
<div class="filter-section">
<label class="section-label">Set:</label>
<div class="checkbox-container" id="equipmentSets">
<!-- Checkbox Sections in Grid Layout -->
<div class="checkbox-sections-container">
<!-- Equipment Sets -->
<div class="filter-card">
<div class="filter-card-header">Equipment Sets</div>
<div class="checkbox-container" id="equipmentSets">
<div class="checkbox-item">
<input type="checkbox" id="set_14" value="14">
<label for="set_14">Adept's</label>
@ -605,11 +666,11 @@
<label for="set_29">Lightning Proof</label>
</div>
</div>
</div>
</div>
<!-- Legendary Cantrips -->
<div class="filter-section">
<label class="section-label">Cantrips:</label>
<!-- Legendary Cantrips -->
<div class="filter-card">
<div class="filter-card-header">Legendary Cantrips</div>
<div class="checkbox-container" id="cantrips">
<!-- Legendary Attributes -->
<div class="checkbox-item">
@ -782,11 +843,11 @@
<label for="cantrip_legendary_storm_bane">Storm Bane</label>
</div>
</div>
</div>
</div>
<!-- Legendary Wards -->
<div class="filter-section">
<label class="section-label">Wards:</label>
<!-- Legendary Wards -->
<div class="filter-card">
<div class="filter-card-header">Legendary Wards</div>
<div class="checkbox-container" id="protections">
<div class="checkbox-item">
<input type="checkbox" id="protection_flame" value="Legendary Flame Ward">
@ -821,15 +882,12 @@
<label for="protection_armor">Armor</label>
</div>
</div>
</div>
</div>
<!-- Equipment Slots -->
<div class="filter-section">
<label class="section-label">Equipment Slots:</label>
<!-- Armor Slots -->
<div class="checkbox-container" id="armor-slots">
<label class="subsection-label">Armor:</label>
<!-- Equipment Slots -->
<div class="filter-card">
<div class="filter-card-header">Equipment Slots</div>
<div class="checkbox-container" id="all-slots">
<div class="checkbox-item">
<input type="checkbox" id="slot_head" value="Head">
<label for="slot_head">Head</label>
@ -870,11 +928,6 @@
<input type="checkbox" id="slot_shield" value="Shield">
<label for="slot_shield">Shield</label>
</div>
</div>
<!-- Jewelry Slots -->
<div class="checkbox-container" id="jewelry-slots">
<label class="subsection-label">Jewelry:</label>
<div class="checkbox-item">
<input type="checkbox" id="slot_neck" value="Neck">
<label for="slot_neck">Neck</label>
@ -892,6 +945,7 @@
<label for="slot_trinket">Trinket</label>
</div>
</div>
</div>
</div>
<div class="search-actions">

View file

@ -255,6 +255,10 @@ function buildSearchParameters() {
params.append('armor_only', 'true');
} else if (equipmentType === 'jewelry') {
params.append('jewelry_only', 'true');
} else if (equipmentType === 'shirt') {
params.append('shirt_only', 'true');
} else if (equipmentType === 'pants') {
params.append('pants_only', 'true');
}
// If 'all' is selected, don't add any type filter
@ -298,6 +302,7 @@ function buildSearchParameters() {
addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost');
addParam(params, 'min_vitality_rating', 'searchMinVitalityRating');
addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating');
addParam(params, 'min_crit_damage_resist_rating', 'searchMinCritDamageResistRating');
// Requirements parameters
addParam(params, 'min_level', 'searchMinLevel');
@ -438,6 +443,7 @@ function displayResults(data) {
<th class="text-right sortable" data-sort="heal_boost_rating">Heal Boost${getSortIcon('heal_boost_rating')}</th>
<th class="text-right sortable" data-sort="vitality_rating">Vitality${getSortIcon('vitality_rating')}</th>
<th class="text-right sortable" data-sort="damage_resist_rating">Dmg Resist${getSortIcon('damage_resist_rating')}</th>
<th class="text-right sortable" data-sort="crit_damage_resist_rating">Crit Dmg Resist${getSortIcon('crit_damage_resist_rating')}</th>
<th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
</tr>
</thead>
@ -451,6 +457,7 @@ function displayResults(data) {
const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-';
const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-';
const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-';
const critDamageResistRating = item.crit_damage_resist_rating > 0 ? item.crit_damage_resist_rating : '-';
const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory';
const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory';
@ -522,6 +529,7 @@ function displayResults(data) {
<td class="text-right">${healBoostRating}</td>
<td class="text-right">${vitalityRating}</td>
<td class="text-right">${damageResistRating}</td>
<td class="text-right">${critDamageResistRating}</td>
<td class="text-right">${lastUpdated}</td>
</tr>
`;

View file

@ -657,6 +657,176 @@ body {
margin-left: 4px;
}
/* Ratings display */
.item-ratings {
color: #0066cc;
font-size: 11px;
font-weight: 600;
background: #e6f3ff;
padding: 1px 4px;
border-radius: 2px;
margin-left: 4px;
}
/* Empty slot styling */
.suit-item-entry.empty-slot {
opacity: 0.6;
background: #f8f9fa;
border-left: 3px solid #dee2e6;
padding-left: 8px;
}
.empty-slot-text {
color: #6c757d;
font-style: italic;
font-size: 11px;
}
/* New Column-Based Table Layout */
.suit-items-table {
width: 100%;
font-size: 12px;
margin-top: 8px;
}
.suit-items-header {
display: grid;
grid-template-columns: 80px 120px 250px 140px 250px 60px 120px;
gap: 8px;
background: #2c3e50;
color: white;
padding: 8px 4px;
font-weight: 600;
font-size: 11px;
border-radius: 4px 4px 0 0;
}
.suit-items-header > div {
color: white !important;
opacity: 1 !important;
}
.suit-items-body {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-top: none;
border-radius: 0 0 4px 4px;
}
.suit-item-row {
display: grid;
grid-template-columns: 80px 120px 250px 140px 250px 60px 120px;
gap: 8px;
padding: 6px 4px;
border-bottom: 1px solid #e9ecef;
align-items: center;
min-height: 24px;
}
.suit-item-row:last-child {
border-bottom: none;
}
.suit-item-row:nth-child(even) {
background: #ffffff;
}
.suit-item-row.empty-slot {
opacity: 0.5;
color: #6c757d;
font-style: italic;
}
/* Column styling */
.col-slot {
font-weight: 600;
color: #495057;
}
.col-character {
color: #666;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-item {
color: #333;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-set {
font-weight: 600;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.suit-items-body .col-set {
color: #1f2937;
background: #f3f4f6;
padding: 2px 4px;
border-radius: 3px;
}
.col-spells {
color: #7c3aed;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-armor {
color: #059669;
font-weight: 600;
text-align: center;
}
.col-ratings {
color: #0066cc;
font-size: 11px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.need-reducing {
color: #dc2626;
font-weight: bold;
margin-left: 2px;
}
/* Responsive adjustments for table */
@media (max-width: 1200px) {
.suit-items-header,
.suit-item-row {
grid-template-columns: 70px 100px 200px 120px 200px 50px 100px;
gap: 6px;
font-size: 11px;
}
}
@media (max-width: 900px) {
.suit-items-header,
.suit-item-row {
grid-template-columns: 60px 80px 150px 100px 150px 40px 80px;
gap: 4px;
font-size: 10px;
}
.col-spells,
.col-ratings {
font-size: 10px;
}
}
/* Progressive Search Styles */
.search-progress {
margin-top: 15px;

View file

@ -1,10 +1,12 @@
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
console.log('Suitbuilder.js loaded - VERSION: SCORE_ORDERING_AND_CANCELLATION_FIX_v3');
// Configuration
const API_BASE = '/inv/suitbuilder';
let currentSuits = [];
let lockedSlots = new Set();
let selectedSuit = null;
let currentSearchController = null; // AbortController for current search
// Initialize when page loads
document.addEventListener('DOMContentLoaded', function() {
@ -163,9 +165,14 @@ async function performSuitSearch() {
try {
await streamOptimalSuits(constraints);
} catch (error) {
console.error('Suit search error:', error);
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
countSpan.textContent = '';
// Don't show error for user-cancelled searches
if (error.name === 'AbortError') {
console.log('Search cancelled by user');
} else {
console.error('Suit search error:', error);
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
countSpan.textContent = '';
}
}
}
@ -260,13 +267,22 @@ async function streamOptimalSuits(constraints) {
console.log('Starting suit search with constraints:', requestBody);
// Cancel any existing search
if (currentSearchController) {
currentSearchController.abort();
}
// Create new AbortController for this search
currentSearchController = new AbortController();
// Use fetch with streaming response instead of EventSource for POST support
const response = await fetch(`${API_BASE}/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
body: JSON.stringify(requestBody),
signal: currentSearchController.signal // Add abort signal
});
if (!response.ok) {
@ -340,7 +356,13 @@ async function streamOptimalSuits(constraints) {
}
}
} catch (error) {
reject(error);
// Don't treat abort as an error
if (error.name === 'AbortError') {
console.log('Search was aborted by user');
resolve();
} else {
reject(error);
}
}
}
@ -349,14 +371,19 @@ async function streamOptimalSuits(constraints) {
// Event handlers
function handleSuitEvent(data) {
try {
console.log('NEW handleSuitEvent called with data:', data);
// Transform backend suit format to frontend format
const transformedSuit = transformSuitData(data);
console.log('Transformed suit:', transformedSuit);
// Insert suit in score-ordered position (highest score first)
insertSuitInScoreOrder(transformedSuit);
const insertIndex = insertSuitInScoreOrder(transformedSuit);
console.log('Insert index returned:', insertIndex);
// Regenerate entire results display to maintain proper ordering
regenerateResultsDisplay();
// Insert DOM element at the correct position instead of regenerating everything
insertSuitDOMAtPosition(transformedSuit, insertIndex);
console.log('DOM insertion complete');
// Update count
document.getElementById('foundCount').textContent = currentSuits.length;
@ -364,6 +391,7 @@ async function streamOptimalSuits(constraints) {
} catch (error) {
console.error('Error processing suit data:', error);
console.error('Stack trace:', error.stack);
}
}
@ -412,6 +440,12 @@ async function streamOptimalSuits(constraints) {
stopButton.addEventListener('click', () => {
searchStopped = true;
// Actually abort the HTTP request
if (currentSearchController) {
currentSearchController.abort();
currentSearchController = null;
}
// Update UI to show search was stopped
const loadingDiv = document.querySelector('.loading');
if (loadingDiv) {
@ -467,6 +501,8 @@ function transformSuitData(suit) {
* Insert a suit into the currentSuits array in score-ordered position (highest first)
*/
function insertSuitInScoreOrder(suit) {
console.log(`Inserting suit with score ${suit.score}. Current suits:`, currentSuits.map(s => s.score));
// Find the correct position to insert the suit (highest score first)
let insertIndex = 0;
for (let i = 0; i < currentSuits.length; i++) {
@ -479,12 +515,17 @@ function insertSuitInScoreOrder(suit) {
// Insert the suit at the correct position
currentSuits.splice(insertIndex, 0, suit);
console.log(`Inserted at index ${insertIndex}. New order:`, currentSuits.map(s => s.score));
return insertIndex;
}
/**
* Regenerate the entire results display to maintain proper score ordering
*/
function regenerateResultsDisplay() {
console.log('Regenerating display with suits:', currentSuits.map(s => `Score: ${s.score}, ID: ${s.id}`));
const streamingResults = document.getElementById('streamingResults');
if (!streamingResults) return;
@ -526,6 +567,79 @@ function regenerateResultsDisplay() {
});
}
/**
* Insert a suit DOM element at the correct position and update all medal rankings
*/
function insertSuitDOMAtPosition(suit, insertIndex) {
console.log('insertSuitDOMAtPosition called with suit score:', suit.score, 'at index:', insertIndex);
const streamingResults = document.getElementById('streamingResults');
if (!streamingResults) {
console.error('streamingResults element not found!');
return;
}
console.log('Current DOM children count:', streamingResults.children.length);
// Create the new suit HTML
const scoreClass = getScoreClass(suit.score);
const suitHtml = `
<div class="suit-item" data-suit-id="${suit.id}">
<div class="suit-header">
<div class="suit-score ${scoreClass}">
🔸 Suit #${suit.id} (Score: ${suit.score})
</div>
</div>
<div class="suit-stats">
${formatSuitStats(suit)}
</div>
<div class="suit-items">
${formatSuitItems(suit.items)}
${suit.missing && suit.missing.length > 0 ? `<div class="missing-items">Missing: ${suit.missing.join(', ')}</div>` : ''}
${suit.notes && suit.notes.length > 0 ? `<div class="suit-notes">${suit.notes.join(' • ')}</div>` : ''}
</div>
</div>
`;
// Insert at the correct position
const existingSuits = streamingResults.children;
if (insertIndex >= existingSuits.length) {
// Insert at the end
streamingResults.insertAdjacentHTML('beforeend', suitHtml);
} else {
// Insert before the suit at insertIndex
existingSuits[insertIndex].insertAdjacentHTML('beforebegin', suitHtml);
}
// Update all medal rankings after insertion
updateAllMedals();
// Add click handler for the new suit
const newSuitElement = streamingResults.children[insertIndex];
newSuitElement.addEventListener('click', function() {
const suitId = parseInt(this.dataset.suitId);
selectSuit(suitId);
});
}
/**
* Update medal rankings for all displayed suits
*/
function updateAllMedals() {
const streamingResults = document.getElementById('streamingResults');
if (!streamingResults) return;
Array.from(streamingResults.children).forEach((suitElement, index) => {
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸';
const scoreElement = suitElement.querySelector('.suit-score');
if (scoreElement) {
const scoreText = scoreElement.textContent;
// Replace the existing medal with the new one
scoreElement.textContent = scoreText.replace(/^[🥇🥈🥉🔸]\s*/, medal + ' ');
}
});
}
/**
* Generate suit combinations from available items
* This is a simplified algorithm - the full constraint solver will be more sophisticated
@ -697,7 +811,7 @@ function getScoreClass(score) {
* Format suit items for display - shows ALL armor slots even if empty
*/
function formatSuitItems(items) {
let html = '';
console.log(`[DEBUG] formatSuitItems called with items:`, items);
// Define all expected armor/equipment slots in logical order
const allSlots = [
@ -710,39 +824,161 @@ function formatSuitItems(items) {
'Shirt', 'Pants'
];
console.log(`[DEBUG] allSlots:`, allSlots);
// Create table structure with header
let html = `
<div class="suit-items-table">
<div class="suit-items-header">
<div class="col-slot">Slot</div>
<div class="col-character">Character</div>
<div class="col-item">Item</div>
<div class="col-set">Set</div>
<div class="col-spells">Spells</div>
<div class="col-armor">Armor</div>
<div class="col-ratings">Ratings</div>
</div>
<div class="suit-items-body">
`;
allSlots.forEach(slot => {
const item = items ? items[slot] : null;
// DEBUG: Log all slots and items
console.log(`[DEBUG] Processing slot '${slot}', item:`, item);
if (item) {
// Item exists in this slot
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
const properties = formatItemProperties(item);
const ratings = formatItemRatings(item);
const character = item.source_character || item.character_name || 'Unknown';
const itemName = item.name || 'Unknown Item';
// Only show set names for armor items (not jewelry or clothing)
const armorSlots = ['Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands',
'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet'];
const setName = (armorSlots.includes(slot) && item.set_name) ? item.set_name : '-';
const spells = formatItemSpells(item);
const armor = formatItemArmor(item);
const ratings = formatItemRatingsColumns(item);
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">*</span>' : '';
html += `
<div class="suit-item-entry">
<strong>${slot}:</strong>
<span class="item-character">${item.source_character || item.character_name || 'Unknown'}</span> -
<span class="item-name">${item.name}</span>
${properties ? `<span class="item-properties">(${properties})</span>` : ''}
${ratings ? `<span class="item-ratings">[${ratings}]</span>` : ''}
${needsReducing}
<div class="suit-item-row">
<div class="col-slot">${slot}</div>
<div class="col-character">${character}</div>
<div class="col-item">${itemName}${needsReducing}</div>
<div class="col-set">${setName}</div>
<div class="col-spells">${spells}</div>
<div class="col-armor">${armor}</div>
<div class="col-ratings">${ratings}</div>
</div>
`;
} else {
// Empty slot
html += `
<div class="suit-item-entry empty-slot">
<strong>${slot}:</strong>
<span class="empty-slot-text">- Empty -</span>
<div class="suit-item-row empty-slot">
<div class="col-slot">${slot}</div>
<div class="col-character">-</div>
<div class="col-item">-</div>
<div class="col-set">-</div>
<div class="col-spells">-</div>
<div class="col-armor">-</div>
<div class="col-ratings">-</div>
</div>
`;
}
});
html += `
</div>
</div>
`;
return html;
}
/**
* Format item spells for column display (focus on Legendary spells)
*/
function formatItemSpells(item) {
const spellArray = item.spells || item.spell_names || [];
if (!Array.isArray(spellArray) || spellArray.length === 0) {
return '-';
}
// Filter for important spells (Legendary, Epic)
const importantSpells = spellArray.filter(spell =>
spell.includes('Legendary') || spell.includes('Epic')
);
if (importantSpells.length === 0) {
return `${spellArray.length} spells`;
}
// Show up to 2 important spells, abbreviate the rest
const displaySpells = importantSpells.slice(0, 2);
let result = displaySpells.join(', ');
if (importantSpells.length > 2) {
result += ` +${importantSpells.length - 2} more`;
}
return result;
}
/**
* Format item armor for column display
*/
function formatItemArmor(item) {
if (item.armor_level && item.armor_level > 0) {
return item.armor_level.toString();
}
return '-';
}
/**
* Format item ratings for column display
*/
function formatItemRatingsColumns(item) {
const ratings = [];
// Access ratings from the ratings object if available, fallback to direct properties
const itemRatings = item.ratings || item;
// Helper function to get rating value, treating null/undefined/negative as 0
function getRatingValue(value) {
if (value === null || value === undefined || value < 0) return 0;
return Math.round(value); // Round to nearest integer
}
// Determine if this is clothing (shirts/pants) or armor
// Check item name patterns since ObjectClass 3 items (clothing) may appear in various slots
const itemName = item.name || '';
const isClothing = itemName.toLowerCase().includes('shirt') ||
itemName.toLowerCase().includes('pants') ||
itemName.toLowerCase().includes('breeches') ||
itemName.toLowerCase().includes('baggy') ||
(item.slot === 'Shirt' || item.slot === 'Pants');
if (isClothing) {
// Clothing: Show DR and DRR
const damageRating = getRatingValue(itemRatings.damage_rating);
const damageResist = getRatingValue(itemRatings.damage_resist_rating);
ratings.push(`DR${damageRating}`);
ratings.push(`DRR${damageResist}`);
} else {
// Armor: Show CD and CDR
const critDamage = getRatingValue(itemRatings.crit_damage_rating);
const critDamageResist = getRatingValue(itemRatings.crit_damage_resist_rating);
ratings.push(`CD${critDamage}`);
ratings.push(`CDR${critDamageResist}`);
}
return ratings.join(' ');
}
/**
* Check if item is multi-slot and needs reducing
* Only armor items need reduction - jewelry can naturally go in multiple slots
@ -918,7 +1154,7 @@ function populateVisualSlots(items) {
slotElement.innerHTML = `
<div class="slot-item-name">${item.name}</div>
<div class="slot-item-character">${item.character_name}</div>
<div class="slot-item-character">${item.source_character || item.character_name || 'Unknown'}</div>
<div class="slot-item-properties">${formatItemProperties(item)}</div>
${needsReducing}
`;