Fix score-based ordering in suitbuilder frontend
Updated JavaScript to maintain score ordering during streaming search: - Replace addSuitToResults() with insertSuitInScoreOrder() - Add regenerateResultsDisplay() to maintain proper DOM ordering - Medal assignment (🥇🥈🥉) now based on score ranking, not arrival order - Suits with highest scores now always appear at top during live search - Updated displaySuitResults() to sort by score before displaying 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4d19e29847
commit
e7ca39318f
1 changed files with 306 additions and 209 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
|
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const API_BASE = '/inv';
|
const API_BASE = '/inv/suitbuilder';
|
||||||
let currentSuits = [];
|
let currentSuits = [];
|
||||||
let lockedSlots = new Set();
|
let lockedSlots = new Set();
|
||||||
let selectedSuit = null;
|
let selectedSuit = null;
|
||||||
|
|
@ -25,7 +25,7 @@ function initializeSuitbuilder() {
|
||||||
*/
|
*/
|
||||||
async function loadCharacters() {
|
async function loadCharacters() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/characters/list`);
|
const response = await fetch(`${API_BASE}/characters`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to load characters');
|
throw new Error('Failed to load characters');
|
||||||
}
|
}
|
||||||
|
|
@ -52,11 +52,13 @@ function displayCharacters(characters) {
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
characters.forEach(character => {
|
characters.forEach(character => {
|
||||||
|
// Sanitize character name for HTML ID (replace special chars with underscores)
|
||||||
|
const safeId = character.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
html += `
|
html += `
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="char_${character.character_name}"
|
<input type="checkbox" id="char_${safeId}"
|
||||||
class="character-checkbox" value="${character.character_name}" checked>
|
class="character-checkbox" value="${character}" checked>
|
||||||
<label for="char_${character.character_name}">${character.character_name}</label>
|
<label for="char_${safeId}">${character}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
@ -234,66 +236,181 @@ function validateConstraints(constraints) {
|
||||||
* Stream optimal suits using Server-Sent Events with progressive results
|
* Stream optimal suits using Server-Sent Events with progressive results
|
||||||
*/
|
*/
|
||||||
async function streamOptimalSuits(constraints) {
|
async function streamOptimalSuits(constraints) {
|
||||||
// Build request parameters for the streaming constraint solver
|
// Prepare constraint data for POST request
|
||||||
const params = new URLSearchParams();
|
const requestBody = {
|
||||||
|
characters: constraints.characters.length > 0 ? constraints.characters : [],
|
||||||
|
primary_set: constraints.primary_set ? parseInt(constraints.primary_set) : null,
|
||||||
|
secondary_set: constraints.secondary_set ? parseInt(constraints.secondary_set) : null,
|
||||||
|
required_spells: [
|
||||||
|
...constraints.legendary_cantrips,
|
||||||
|
...constraints.protection_spells
|
||||||
|
],
|
||||||
|
locked_items: {}, // TODO: implement locked items
|
||||||
|
include_equipped: constraints.include_equipped,
|
||||||
|
include_inventory: constraints.include_inventory,
|
||||||
|
min_armor: constraints.min_armor ? parseInt(constraints.min_armor) : null,
|
||||||
|
max_armor: constraints.max_armor ? parseInt(constraints.max_armor) : null,
|
||||||
|
min_crit_damage: constraints.min_crit_damage ? parseInt(constraints.min_crit_damage) : null,
|
||||||
|
max_crit_damage: constraints.max_crit_damage ? parseInt(constraints.max_crit_damage) : null,
|
||||||
|
min_damage_rating: constraints.min_damage_rating ? parseInt(constraints.min_damage_rating) : null,
|
||||||
|
max_damage_rating: constraints.max_damage_rating ? parseInt(constraints.max_damage_rating) : null,
|
||||||
|
max_results: 10,
|
||||||
|
search_timeout: 300
|
||||||
|
};
|
||||||
|
|
||||||
// Character selection
|
console.log('Starting suit search with constraints:', requestBody);
|
||||||
if (constraints.characters.length > 0) {
|
|
||||||
params.append('characters', constraints.characters.join(','));
|
// Use fetch with streaming response instead of EventSource for POST support
|
||||||
} else {
|
const response = await fetch(`${API_BASE}/search`, {
|
||||||
params.append('include_all_characters', 'true');
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search failed: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equipment sets
|
const reader = response.body.getReader();
|
||||||
if (constraints.primary_set) {
|
const decoder = new TextDecoder();
|
||||||
params.append('primary_set', constraints.primary_set);
|
|
||||||
}
|
|
||||||
if (constraints.secondary_set) {
|
|
||||||
params.append('secondary_set', constraints.secondary_set);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legendary cantrips
|
|
||||||
if (constraints.legendary_cantrips.length > 0) {
|
|
||||||
params.append('legendary_cantrips', constraints.legendary_cantrips.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legendary wards
|
|
||||||
if (constraints.protection_spells.length > 0) {
|
|
||||||
params.append('legendary_wards', constraints.protection_spells.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rating constraints
|
|
||||||
if (constraints.min_armor) params.append('min_armor', constraints.min_armor);
|
|
||||||
if (constraints.max_armor) params.append('max_armor', constraints.max_armor);
|
|
||||||
if (constraints.min_crit_damage) params.append('min_crit_damage', constraints.min_crit_damage);
|
|
||||||
if (constraints.max_crit_damage) params.append('max_crit_damage', constraints.max_crit_damage);
|
|
||||||
if (constraints.min_damage_rating) params.append('min_damage_rating', constraints.min_damage_rating);
|
|
||||||
if (constraints.max_damage_rating) params.append('max_damage_rating', constraints.max_damage_rating);
|
|
||||||
|
|
||||||
// Equipment status
|
|
||||||
params.append('include_equipped', constraints.include_equipped.toString());
|
|
||||||
params.append('include_inventory', constraints.include_inventory.toString());
|
|
||||||
|
|
||||||
// Locked slots
|
|
||||||
if (lockedSlots.size > 0) {
|
|
||||||
params.append('locked_slots', Array.from(lockedSlots).join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search depth (default to balanced)
|
|
||||||
params.append('search_depth', 'balanced');
|
|
||||||
|
|
||||||
const streamUrl = `${API_BASE}/optimize/suits/stream?${params.toString()}`;
|
|
||||||
console.log('Streaming suits with URL:', streamUrl);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const eventSource = new EventSource(streamUrl);
|
|
||||||
let searchStopped = false;
|
let searchStopped = false;
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
async function readStream() {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
resolve();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchStopped) {
|
||||||
|
await reader.cancel();
|
||||||
|
resolve();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process SSE data
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||||
|
|
||||||
|
let currentEventType = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('event: ')) {
|
||||||
|
currentEventType = line.substring(7).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const data = line.substring(6);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventData = JSON.parse(data);
|
||||||
|
|
||||||
|
// Handle different event types based on the current event type
|
||||||
|
if (currentEventType === 'suit') {
|
||||||
|
handleSuitEvent(eventData);
|
||||||
|
} else if (currentEventType === 'progress') {
|
||||||
|
handleProgressEvent(eventData);
|
||||||
|
} else if (currentEventType === 'complete') {
|
||||||
|
handleCompleteEvent(eventData);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
} else if (currentEventType === 'error') {
|
||||||
|
handleErrorEvent(eventData);
|
||||||
|
reject(new Error(eventData.message || 'Search error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset event type after processing
|
||||||
|
currentEventType = null;
|
||||||
|
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('Failed to parse SSE data:', data, 'Event type:', currentEventType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readStream();
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
function handleSuitEvent(data) {
|
||||||
|
try {
|
||||||
|
// Transform backend suit format to frontend format
|
||||||
|
const transformedSuit = transformSuitData(data);
|
||||||
|
|
||||||
|
// Insert suit in score-ordered position (highest score first)
|
||||||
|
insertSuitInScoreOrder(transformedSuit);
|
||||||
|
|
||||||
|
// Regenerate entire results display to maintain proper ordering
|
||||||
|
regenerateResultsDisplay();
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
document.getElementById('foundCount').textContent = currentSuits.length;
|
||||||
|
document.getElementById('resultsCount').textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing suit data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProgressEvent(data) {
|
||||||
|
try {
|
||||||
|
document.getElementById('foundCount').textContent = data.found || currentSuits.length;
|
||||||
|
document.getElementById('checkedCount').textContent = data.evaluated || 0;
|
||||||
|
document.getElementById('elapsedTime').textContent = data.elapsed || '0.0';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing progress data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCompleteEvent(data) {
|
||||||
|
try {
|
||||||
|
// Hide loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) {
|
||||||
|
loadingDiv.innerHTML = `✅ Search complete! Found ${data.suits_found} suits in ${data.duration}s.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update final results count
|
||||||
|
const countSpan = document.getElementById('resultsCount');
|
||||||
|
if (countSpan) {
|
||||||
|
countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing completion data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleErrorEvent(data) {
|
||||||
|
try {
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) {
|
||||||
|
loadingDiv.innerHTML = `❌ Search error: ${data.message}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing error data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add stop search functionality
|
// Add stop search functionality
|
||||||
const stopButton = document.getElementById('stopSearch');
|
const stopButton = document.getElementById('stopSearch');
|
||||||
stopButton.addEventListener('click', () => {
|
stopButton.addEventListener('click', () => {
|
||||||
searchStopped = true;
|
searchStopped = true;
|
||||||
eventSource.close();
|
|
||||||
|
|
||||||
// Update UI to show search was stopped
|
// Update UI to show search was stopped
|
||||||
const loadingDiv = document.querySelector('.loading');
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
|
@ -306,118 +423,7 @@ async function streamOptimalSuits(constraints) {
|
||||||
if (countSpan) {
|
if (countSpan) {
|
||||||
countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''} (search stopped)`;
|
countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''} (search stopped)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle individual suit results
|
|
||||||
eventSource.addEventListener('suit', (event) => {
|
|
||||||
try {
|
|
||||||
const suit = JSON.parse(event.data);
|
|
||||||
|
|
||||||
// Transform backend suit format to frontend format
|
|
||||||
const transformedSuit = transformSuitData(suit);
|
|
||||||
currentSuits.push(transformedSuit);
|
|
||||||
|
|
||||||
// Add suit to streaming results
|
|
||||||
addSuitToResults(transformedSuit, currentSuits.length - 1);
|
|
||||||
|
|
||||||
// Update count
|
|
||||||
document.getElementById('foundCount').textContent = currentSuits.length;
|
|
||||||
document.getElementById('resultsCount').textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing suit data:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle progress updates
|
|
||||||
eventSource.addEventListener('progress', (event) => {
|
|
||||||
try {
|
|
||||||
const progress = JSON.parse(event.data);
|
|
||||||
document.getElementById('foundCount').textContent = progress.found || currentSuits.length;
|
|
||||||
document.getElementById('checkedCount').textContent = progress.checked || 0;
|
|
||||||
document.getElementById('elapsedTime').textContent = progress.elapsed || '0.0';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing progress data:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle search completion
|
|
||||||
eventSource.addEventListener('complete', (event) => {
|
|
||||||
try {
|
|
||||||
const completion = JSON.parse(event.data);
|
|
||||||
|
|
||||||
// Hide loading indicator
|
|
||||||
const loadingDiv = document.querySelector('.loading');
|
|
||||||
if (loadingDiv) {
|
|
||||||
loadingDiv.innerHTML = `✅ Search complete! Found ${completion.total_found} suits in ${completion.total_time}s.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update final results count
|
|
||||||
const countSpan = document.getElementById('resultsCount');
|
|
||||||
if (countSpan) {
|
|
||||||
countSpan.textContent = `Found ${currentSuits.length} suit${currentSuits.length !== 1 ? 's' : ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSource.close();
|
|
||||||
resolve();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing completion data:', error);
|
|
||||||
eventSource.close();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle timeout
|
|
||||||
eventSource.addEventListener('timeout', (event) => {
|
|
||||||
try {
|
|
||||||
const timeout = JSON.parse(event.data);
|
|
||||||
|
|
||||||
// Update UI to show timeout
|
|
||||||
const loadingDiv = document.querySelector('.loading');
|
|
||||||
if (loadingDiv) {
|
|
||||||
loadingDiv.innerHTML = `⏰ ${timeout.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSource.close();
|
|
||||||
resolve();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing timeout data:', error);
|
|
||||||
eventSource.close();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors
|
|
||||||
eventSource.addEventListener('error', (event) => {
|
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(event.data);
|
|
||||||
console.error('Stream error:', errorData.message);
|
|
||||||
|
|
||||||
const loadingDiv = document.querySelector('.loading');
|
|
||||||
if (loadingDiv) {
|
|
||||||
loadingDiv.innerHTML = `❌ Search error: ${errorData.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing error data:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSource.close();
|
|
||||||
reject(new Error('Stream error occurred'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle connection errors
|
|
||||||
eventSource.onerror = (event) => {
|
|
||||||
if (!searchStopped) {
|
|
||||||
console.error('EventSource error:', event);
|
|
||||||
eventSource.close();
|
|
||||||
reject(new Error('Connection error during streaming'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -458,12 +464,35 @@ function transformSuitData(suit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a single suit to the streaming results display
|
* Insert a suit into the currentSuits array in score-ordered position (highest first)
|
||||||
*/
|
*/
|
||||||
function addSuitToResults(suit, index) {
|
function insertSuitInScoreOrder(suit) {
|
||||||
|
// Find the correct position to insert the suit (highest score first)
|
||||||
|
let insertIndex = 0;
|
||||||
|
for (let i = 0; i < currentSuits.length; i++) {
|
||||||
|
if (suit.score > currentSuits[i].score) {
|
||||||
|
insertIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
insertIndex = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the suit at the correct position
|
||||||
|
currentSuits.splice(insertIndex, 0, suit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the entire results display to maintain proper score ordering
|
||||||
|
*/
|
||||||
|
function regenerateResultsDisplay() {
|
||||||
const streamingResults = document.getElementById('streamingResults');
|
const streamingResults = document.getElementById('streamingResults');
|
||||||
if (!streamingResults) return;
|
if (!streamingResults) return;
|
||||||
|
|
||||||
|
// Clear existing results
|
||||||
|
streamingResults.innerHTML = '';
|
||||||
|
|
||||||
|
// Re-add all suits in their current (score-ordered) positions
|
||||||
|
currentSuits.forEach((suit, index) => {
|
||||||
const scoreClass = getScoreClass(suit.score);
|
const scoreClass = getScoreClass(suit.score);
|
||||||
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸';
|
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸';
|
||||||
|
|
||||||
|
|
@ -486,13 +515,15 @@ function addSuitToResults(suit, index) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
streamingResults.insertAdjacentHTML('beforeend', suitHtml);
|
streamingResults.insertAdjacentHTML('beforeend', suitHtml);
|
||||||
|
});
|
||||||
|
|
||||||
// Add click handler for the new suit
|
// Re-add click handlers for all suits
|
||||||
const newSuitElement = streamingResults.lastElementChild;
|
document.querySelectorAll('.suit-item').forEach(item => {
|
||||||
newSuitElement.addEventListener('click', function() {
|
item.addEventListener('click', function() {
|
||||||
const suitId = parseInt(this.dataset.suitId);
|
const suitId = parseInt(this.dataset.suitId);
|
||||||
selectSuit(suitId);
|
selectSuit(suitId);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -612,10 +643,13 @@ function displaySuitResults(suits) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
countSpan.textContent = `Found ${suits.length} suit${suits.length !== 1 ? 's' : ''}`;
|
// Sort suits by score (highest first) before displaying
|
||||||
|
const sortedSuits = [...suits].sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
countSpan.textContent = `Found ${sortedSuits.length} suit${sortedSuits.length !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
suits.forEach((suit, index) => {
|
sortedSuits.forEach((suit, index) => {
|
||||||
const scoreClass = getScoreClass(suit.score);
|
const scoreClass = getScoreClass(suit.score);
|
||||||
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸';
|
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸';
|
||||||
|
|
||||||
|
|
@ -660,28 +694,50 @@ function getScoreClass(score) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format suit items for display
|
* Format suit items for display - shows ALL armor slots even if empty
|
||||||
*/
|
*/
|
||||||
function formatSuitItems(items) {
|
function formatSuitItems(items) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (!items || Object.keys(items).length === 0) {
|
// Define all expected armor/equipment slots in logical order
|
||||||
return '<div class="no-items">No items in this suit</div>';
|
const allSlots = [
|
||||||
}
|
// Armor slots
|
||||||
|
'Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands',
|
||||||
|
'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet',
|
||||||
|
// Jewelry slots
|
||||||
|
'Neck', 'Left Ring', 'Right Ring', 'Left Wrist', 'Right Wrist', 'Trinket',
|
||||||
|
// Clothing slots
|
||||||
|
'Shirt', 'Pants'
|
||||||
|
];
|
||||||
|
|
||||||
Object.entries(items).forEach(([slot, item]) => {
|
allSlots.forEach(slot => {
|
||||||
|
const item = items ? items[slot] : null;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
// Item exists in this slot
|
||||||
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
|
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
|
||||||
const properties = formatItemProperties(item);
|
const properties = formatItemProperties(item);
|
||||||
|
const ratings = formatItemRatings(item);
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="suit-item-entry">
|
<div class="suit-item-entry">
|
||||||
<strong>${slot}:</strong>
|
<strong>${slot}:</strong>
|
||||||
<span class="item-character">${item.character_name}</span> -
|
<span class="item-character">${item.source_character || item.character_name || 'Unknown'}</span> -
|
||||||
<span class="item-name">${item.name}</span>
|
<span class="item-name">${item.name}</span>
|
||||||
${properties ? `<span class="item-properties">(${properties})</span>` : ''}
|
${properties ? `<span class="item-properties">(${properties})</span>` : ''}
|
||||||
|
${ratings ? `<span class="item-ratings">[${ratings}]</span>` : ''}
|
||||||
${needsReducing}
|
${needsReducing}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
} else {
|
||||||
|
// Empty slot
|
||||||
|
html += `
|
||||||
|
<div class="suit-item-entry empty-slot">
|
||||||
|
<strong>${slot}:</strong>
|
||||||
|
<span class="empty-slot-text">- Empty -</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|
@ -781,6 +837,47 @@ function formatItemProperties(item) {
|
||||||
return properties.join(', ');
|
return properties.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format item ratings for display (separate from properties)
|
||||||
|
*/
|
||||||
|
function formatItemRatings(item) {
|
||||||
|
const ratings = [];
|
||||||
|
|
||||||
|
// Armor level
|
||||||
|
if (item.armor_level && item.armor_level > 0) {
|
||||||
|
ratings.push(`AL ${item.armor_level}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Damage ratings
|
||||||
|
if (item.crit_damage_rating && item.crit_damage_rating > 0) {
|
||||||
|
ratings.push(`CD +${item.crit_damage_rating}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.damage_rating && item.damage_rating > 0) {
|
||||||
|
ratings.push(`DR +${item.damage_rating}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resist ratings
|
||||||
|
if (item.crit_damage_resist_rating && item.crit_damage_resist_rating > 0) {
|
||||||
|
ratings.push(`CDR +${item.crit_damage_resist_rating}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.damage_resist_rating && item.damage_resist_rating > 0) {
|
||||||
|
ratings.push(`DRR +${item.damage_resist_rating}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other ratings
|
||||||
|
if (item.heal_boost_rating && item.heal_boost_rating > 0) {
|
||||||
|
ratings.push(`HB +${item.heal_boost_rating}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.vitality_rating && item.vitality_rating > 0) {
|
||||||
|
ratings.push(`VIT +${item.vitality_rating}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratings.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a suit and populate the visual slots
|
* Select a suit and populate the visual slots
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue