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
|
|
@ -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