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:
parent
e7ca39318f
commit
6c646719dd
6 changed files with 1093 additions and 232 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue