1099 lines
41 KiB
JavaScript
1099 lines
41 KiB
JavaScript
/**
|
||
* Main application script for FFXI Trust Characters web app
|
||
*/
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// DOM elements
|
||
const rolesList = document.getElementById('roles-list');
|
||
const charactersGrid = document.getElementById('characters-grid');
|
||
const characterInfo = document.getElementById('character-info');
|
||
const currentRoleHeading = document.getElementById('current-role');
|
||
const searchInput = document.getElementById('search-input');
|
||
|
||
// Current state
|
||
let currentRole = 'All';
|
||
let currentCharacter = null;
|
||
let trustData = null;
|
||
let currentView = 'all'; // 'all', 'acquired', or 'unacquired'
|
||
|
||
// Role-specific CSS class mapping
|
||
const roleClasses = {
|
||
'Tank': 'Tank',
|
||
'Melee Fighter': 'Melee',
|
||
'Ranged Fighter': 'Ranged',
|
||
'Offensive Caster': 'Offensive',
|
||
'Healer': 'Healer',
|
||
'Support': 'Support',
|
||
'Special': 'Special',
|
||
'Unity Concord': 'Unity'
|
||
};
|
||
|
||
/**
|
||
* Initialize the application
|
||
*/
|
||
function init() {
|
||
// Load trust data
|
||
trustParser.loadData(data => {
|
||
trustData = data;
|
||
|
||
// Populate roles list
|
||
populateRolesList();
|
||
|
||
// Show all characters initially
|
||
showAllCharacters();
|
||
|
||
// Show synergy sets
|
||
showSynergySets();
|
||
|
||
// Set up event listeners
|
||
setupEventListeners();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Identify and group synergy sets
|
||
* @returns {Array} - Array of synergy sets
|
||
*/
|
||
function identifySynergySets() {
|
||
// Create a map to track processed characters
|
||
const processedChars = new Set();
|
||
const synergySets = [];
|
||
|
||
// Process each character
|
||
Object.values(trustData.characters).forEach(character => {
|
||
// Skip if already processed or no synergy names
|
||
if (processedChars.has(character.name) ||
|
||
!character.trustSynergyNames ||
|
||
character.trustSynergyNames.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// Create a new set with this character and its synergy characters
|
||
const setMembers = new Set([character.name]);
|
||
|
||
// Add all synergy characters
|
||
character.trustSynergyNames.forEach(synergyName => {
|
||
setMembers.add(synergyName);
|
||
|
||
// Also add any characters that have synergy with these synergy characters
|
||
const synergyChar = trustData.characters[synergyName];
|
||
if (synergyChar && synergyChar.trustSynergyNames) {
|
||
synergyChar.trustSynergyNames.forEach(nestedName => {
|
||
if (setMembers.has(nestedName)) {
|
||
setMembers.add(nestedName);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Convert set to array and sort
|
||
const setArray = Array.from(setMembers).sort();
|
||
|
||
// Skip sets with only one character
|
||
if (setArray.length <= 1) {
|
||
return;
|
||
}
|
||
|
||
// Create a unique key for this set to avoid duplicates
|
||
const setKey = setArray.join('|');
|
||
|
||
// Check if this set is already included
|
||
const existingSetIndex = synergySets.findIndex(set => {
|
||
const existingKey = set.characters.join('|');
|
||
return existingKey === setKey;
|
||
});
|
||
|
||
if (existingSetIndex === -1) {
|
||
// Add new set
|
||
synergySets.push({
|
||
name: `${setArray.join(' / ')} Synergy`,
|
||
characters: setArray
|
||
});
|
||
|
||
// Mark all characters in this set as processed
|
||
setArray.forEach(name => processedChars.add(name));
|
||
}
|
||
});
|
||
|
||
return synergySets;
|
||
}
|
||
|
||
/**
|
||
* Show synergy sets in the synergy sets container
|
||
*/
|
||
function showSynergySets() {
|
||
const synergySetsContainer = document.getElementById('synergy-sets');
|
||
|
||
// Clear the container
|
||
synergySetsContainer.innerHTML = '';
|
||
|
||
// Get synergy sets
|
||
const synergySets = identifySynergySets();
|
||
|
||
// If no synergy sets, show a message
|
||
if (synergySets.length === 0) {
|
||
synergySetsContainer.innerHTML = `
|
||
<div class="placeholder-message">
|
||
<p>No synergy sets found</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Add each synergy set
|
||
synergySets.forEach(set => {
|
||
const setElement = document.createElement('div');
|
||
setElement.className = 'synergy-set';
|
||
|
||
// Add title
|
||
const titleElement = document.createElement('div');
|
||
titleElement.className = 'synergy-set-title';
|
||
titleElement.textContent = set.name;
|
||
setElement.appendChild(titleElement);
|
||
|
||
// Add characters
|
||
const charactersElement = document.createElement('div');
|
||
charactersElement.className = 'synergy-set-characters';
|
||
|
||
set.characters.forEach(name => {
|
||
const character = trustData.characters[name];
|
||
if (!character) return;
|
||
|
||
const charElement = document.createElement('span');
|
||
charElement.className = 'synergy-set-character';
|
||
charElement.dataset.name = name;
|
||
charElement.textContent = name;
|
||
|
||
// Add role class
|
||
if (character.role && roleClasses[character.role]) {
|
||
charElement.classList.add(roleClasses[character.role]);
|
||
}
|
||
|
||
// Add acquired class if the character is acquired
|
||
if (character.acquired) {
|
||
charElement.classList.add('acquired');
|
||
}
|
||
|
||
charactersElement.appendChild(charElement);
|
||
});
|
||
|
||
setElement.appendChild(charactersElement);
|
||
synergySetsContainer.appendChild(setElement);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Populate the roles list with data from the parser
|
||
*/
|
||
function populateRolesList() {
|
||
// Add "All" option
|
||
const allItem = document.createElement('li');
|
||
allItem.textContent = 'All';
|
||
allItem.classList.add('active');
|
||
allItem.dataset.role = 'All';
|
||
rolesList.appendChild(allItem);
|
||
|
||
// Add "Synergy Sets" option
|
||
const synergyItem = document.createElement('li');
|
||
synergyItem.textContent = 'Synergy Sets';
|
||
synergyItem.dataset.role = 'Synergy';
|
||
synergyItem.classList.add('Synergy');
|
||
rolesList.appendChild(synergyItem);
|
||
|
||
// Define the order of roles
|
||
const roleOrder = [
|
||
'Tank',
|
||
'Melee Fighter',
|
||
'Ranged Fighter',
|
||
'Offensive Caster',
|
||
'Healer',
|
||
'Support',
|
||
'Unity Concord',
|
||
'Special'
|
||
];
|
||
|
||
// Add roles in the specified order
|
||
roleOrder.forEach(role => {
|
||
// Skip if the role doesn't exist in the data
|
||
if (!trustData.roles.includes(role)) return;
|
||
|
||
const li = document.createElement('li');
|
||
li.textContent = role;
|
||
li.dataset.role = role;
|
||
if (roleClasses[role]) {
|
||
li.classList.add(roleClasses[role]);
|
||
}
|
||
rolesList.appendChild(li);
|
||
});
|
||
|
||
// Add any remaining roles that weren't in the specified order
|
||
trustData.roles.forEach(role => {
|
||
if (!roleOrder.includes(role)) {
|
||
const li = document.createElement('li');
|
||
li.textContent = role;
|
||
li.dataset.role = role;
|
||
if (roleClasses[role]) {
|
||
li.classList.add(roleClasses[role]);
|
||
}
|
||
rolesList.appendChild(li);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show all characters in the grid, grouped by role
|
||
*/
|
||
function showAllCharacters() {
|
||
// Clear the grid
|
||
charactersGrid.innerHTML = '';
|
||
|
||
// Show the synergy sets container
|
||
document.getElementById('synergy-sets-container').style.display = 'none';
|
||
|
||
// Define the order of roles
|
||
const roleOrder = [
|
||
'Tank',
|
||
'Melee Fighter',
|
||
'Ranged Fighter',
|
||
'Offensive Caster',
|
||
'Healer',
|
||
'Support',
|
||
'Unity Concord',
|
||
'Special'
|
||
];
|
||
|
||
// Filter roles that exist in the data
|
||
const orderedRoles = roleOrder.filter(role => trustData.roles.includes(role));
|
||
|
||
// Add any remaining roles that weren't in the specified order
|
||
const remainingRoles = trustData.roles.filter(role => !roleOrder.includes(role)).sort();
|
||
const allRoles = [...orderedRoles, ...remainingRoles];
|
||
|
||
// For each role, create a section
|
||
allRoles.forEach(role => {
|
||
// Create section header
|
||
const sectionHeader = document.createElement('div');
|
||
sectionHeader.className = 'role-section-header';
|
||
if (roleClasses[role]) {
|
||
sectionHeader.classList.add(roleClasses[role]);
|
||
}
|
||
|
||
// Add expand/collapse icon
|
||
const expandIcon = document.createElement('span');
|
||
expandIcon.className = 'expand-icon';
|
||
expandIcon.textContent = '−'; // Unicode minus sign (expanded by default)
|
||
sectionHeader.appendChild(expandIcon);
|
||
|
||
// Add role name
|
||
const roleName = document.createElement('span');
|
||
roleName.textContent = role;
|
||
sectionHeader.appendChild(roleName);
|
||
|
||
// Add character count
|
||
const charCount = trustData.charactersByRole[role] ? trustData.charactersByRole[role].length : 0;
|
||
const countSpan = document.createElement('span');
|
||
countSpan.className = 'character-count';
|
||
countSpan.textContent = `(${charCount})`;
|
||
sectionHeader.appendChild(countSpan);
|
||
|
||
// Add section header to grid
|
||
charactersGrid.appendChild(sectionHeader);
|
||
|
||
// Create container for characters in this role
|
||
const charactersContainer = document.createElement('div');
|
||
charactersContainer.className = 'role-characters-container';
|
||
charactersGrid.appendChild(charactersContainer);
|
||
|
||
// Get characters for this role and filter by view
|
||
let characters = trustData.charactersByRole[role] || [];
|
||
characters = filterCharactersByView(characters);
|
||
characters.sort(); // Sort character names alphabetically
|
||
|
||
// Skip this role if no characters match the current view
|
||
if (characters.length === 0) {
|
||
// Remove the section header and container
|
||
charactersGrid.removeChild(sectionHeader);
|
||
charactersGrid.removeChild(charactersContainer);
|
||
return;
|
||
}
|
||
|
||
// Update the character count to reflect filtered results
|
||
countSpan.textContent = `(${characters.length})`;
|
||
|
||
characters.forEach(name => {
|
||
const fullCharacter = trustData.characters[name];
|
||
const card = document.createElement('div');
|
||
card.className = 'character-card';
|
||
|
||
// Add role class
|
||
if (roleClasses[role]) {
|
||
card.classList.add(roleClasses[role]);
|
||
}
|
||
|
||
// Add acquired class if the character is acquired
|
||
if (fullCharacter && fullCharacter.acquired) {
|
||
card.classList.add('acquired');
|
||
}
|
||
|
||
card.dataset.name = name;
|
||
card.textContent = name;
|
||
|
||
// If this is the current character, mark it as active
|
||
if (currentCharacter && currentCharacter.name === name) {
|
||
card.classList.add('active');
|
||
}
|
||
|
||
charactersContainer.appendChild(card);
|
||
});
|
||
});
|
||
|
||
currentRoleHeading.textContent = 'All Characters';
|
||
}
|
||
|
||
/**
|
||
* Show characters for a specific role
|
||
* @param {string} role - Role name
|
||
*/
|
||
function showCharactersByRole(role) {
|
||
// Clear the grid
|
||
charactersGrid.innerHTML = '';
|
||
|
||
// Show the synergy sets container
|
||
document.getElementById('synergy-sets-container').style.display = 'block';
|
||
|
||
// Create section header
|
||
const sectionHeader = document.createElement('div');
|
||
sectionHeader.className = 'role-section-header';
|
||
if (roleClasses[role]) {
|
||
sectionHeader.classList.add(roleClasses[role]);
|
||
}
|
||
|
||
// Add expand/collapse icon
|
||
const expandIcon = document.createElement('span');
|
||
expandIcon.className = 'expand-icon';
|
||
expandIcon.textContent = '−'; // Unicode minus sign (expanded by default)
|
||
sectionHeader.appendChild(expandIcon);
|
||
|
||
// Add role name
|
||
const roleName = document.createElement('span');
|
||
roleName.textContent = role;
|
||
sectionHeader.appendChild(roleName);
|
||
|
||
// Add character count
|
||
const charCount = trustData.charactersByRole[role] ? trustData.charactersByRole[role].length : 0;
|
||
const countSpan = document.createElement('span');
|
||
countSpan.className = 'character-count';
|
||
countSpan.textContent = `(${charCount})`;
|
||
sectionHeader.appendChild(countSpan);
|
||
|
||
// Add section header to grid
|
||
charactersGrid.appendChild(sectionHeader);
|
||
|
||
// Create container for characters in this role
|
||
const charactersContainer = document.createElement('div');
|
||
charactersContainer.className = 'role-characters-container';
|
||
charactersGrid.appendChild(charactersContainer);
|
||
|
||
// Get characters for this role and filter by view
|
||
let characters = trustData.charactersByRole[role] || [];
|
||
characters = filterCharactersByView(characters);
|
||
characters.sort(); // Sort character names alphabetically
|
||
|
||
// If no characters match the current view, show a message
|
||
if (characters.length === 0) {
|
||
const noResults = document.createElement('div');
|
||
noResults.className = 'placeholder-message';
|
||
noResults.innerHTML = `<p>No ${currentView === 'acquired' ? 'acquired' : 'unacquired'} characters found in this role</p>`;
|
||
charactersContainer.appendChild(noResults);
|
||
|
||
// Update the character count to reflect filtered results
|
||
countSpan.textContent = `(0)`;
|
||
return;
|
||
}
|
||
|
||
// Update the character count to reflect filtered results
|
||
countSpan.textContent = `(${characters.length})`;
|
||
|
||
characters.forEach(name => {
|
||
const fullCharacter = trustData.characters[name];
|
||
const card = document.createElement('div');
|
||
card.className = 'character-card';
|
||
|
||
// Add role class
|
||
if (roleClasses[role]) {
|
||
card.classList.add(roleClasses[role]);
|
||
}
|
||
|
||
// Add acquired class if the character is acquired
|
||
if (fullCharacter && fullCharacter.acquired) {
|
||
card.classList.add('acquired');
|
||
}
|
||
|
||
card.dataset.name = name;
|
||
card.textContent = name;
|
||
|
||
// If this is the current character, mark it as active
|
||
if (currentCharacter && currentCharacter.name === name) {
|
||
card.classList.add('active');
|
||
}
|
||
|
||
charactersContainer.appendChild(card);
|
||
});
|
||
|
||
currentRoleHeading.textContent = `${role} Characters`;
|
||
}
|
||
|
||
/**
|
||
* Show a list of characters in the grid, grouped by role
|
||
* @param {Array} characters - Array of character objects
|
||
*/
|
||
function showCharacters(characters) {
|
||
// Clear the grid
|
||
charactersGrid.innerHTML = '';
|
||
|
||
// Show the synergy sets container
|
||
document.getElementById('synergy-sets-container').style.display = 'block';
|
||
|
||
// Group characters by role
|
||
const charactersByRole = {};
|
||
|
||
characters.forEach(char => {
|
||
const fullCharacter = trustData.characters[char.name];
|
||
if (fullCharacter) {
|
||
const role = fullCharacter.role || 'Unknown';
|
||
if (!charactersByRole[role]) {
|
||
charactersByRole[role] = [];
|
||
}
|
||
charactersByRole[role].push(fullCharacter);
|
||
}
|
||
});
|
||
|
||
// Define the order of roles
|
||
const roleOrder = [
|
||
'Tank',
|
||
'Melee Fighter',
|
||
'Ranged Fighter',
|
||
'Offensive Caster',
|
||
'Healer',
|
||
'Support',
|
||
'Unity Concord',
|
||
'Special'
|
||
];
|
||
|
||
// Filter roles that exist in the search results
|
||
const orderedRoles = roleOrder.filter(role => charactersByRole[role] && charactersByRole[role].length > 0);
|
||
|
||
// Add any remaining roles that weren't in the specified order
|
||
const remainingRoles = Object.keys(charactersByRole)
|
||
.filter(role => !roleOrder.includes(role))
|
||
.sort();
|
||
|
||
const allRoles = [...orderedRoles, ...remainingRoles];
|
||
|
||
// For each role with characters, create a section
|
||
allRoles.forEach(role => {
|
||
const charactersInRole = charactersByRole[role];
|
||
|
||
// Skip if no characters in this role
|
||
if (!charactersInRole || charactersInRole.length === 0) return;
|
||
|
||
// Create section header
|
||
const sectionHeader = document.createElement('div');
|
||
sectionHeader.className = 'role-section-header';
|
||
if (roleClasses[role]) {
|
||
sectionHeader.classList.add(roleClasses[role]);
|
||
}
|
||
|
||
// Add expand/collapse icon
|
||
const expandIcon = document.createElement('span');
|
||
expandIcon.className = 'expand-icon';
|
||
expandIcon.textContent = '−'; // Unicode minus sign (expanded by default)
|
||
sectionHeader.appendChild(expandIcon);
|
||
|
||
// Add role name
|
||
const roleName = document.createElement('span');
|
||
roleName.textContent = role;
|
||
sectionHeader.appendChild(roleName);
|
||
|
||
// Add character count
|
||
const countSpan = document.createElement('span');
|
||
countSpan.className = 'character-count';
|
||
countSpan.textContent = `(${charactersInRole.length})`;
|
||
sectionHeader.appendChild(countSpan);
|
||
|
||
// Add section header to grid
|
||
charactersGrid.appendChild(sectionHeader);
|
||
|
||
// Create container for characters in this role
|
||
const charactersContainer = document.createElement('div');
|
||
charactersContainer.className = 'role-characters-container';
|
||
charactersGrid.appendChild(charactersContainer);
|
||
|
||
// Sort characters by name
|
||
charactersInRole.sort((a, b) => a.name.localeCompare(b.name));
|
||
|
||
// Add characters for this role
|
||
charactersInRole.forEach(character => {
|
||
const card = document.createElement('div');
|
||
card.className = 'character-card';
|
||
|
||
// Add role class
|
||
if (roleClasses[role]) {
|
||
card.classList.add(roleClasses[role]);
|
||
}
|
||
|
||
// Add acquired class if the character is acquired
|
||
if (character.acquired) {
|
||
card.classList.add('acquired');
|
||
}
|
||
|
||
card.dataset.name = character.name;
|
||
card.textContent = character.name;
|
||
|
||
// If this is the current character, mark it as active
|
||
if (currentCharacter && currentCharacter.name === character.name) {
|
||
card.classList.add('active');
|
||
}
|
||
|
||
charactersContainer.appendChild(card);
|
||
});
|
||
});
|
||
|
||
// If no characters were found, show a message
|
||
if (allRoles.length === 0) {
|
||
const noResults = document.createElement('div');
|
||
noResults.className = 'placeholder-message';
|
||
noResults.innerHTML = '<p>No characters found</p>';
|
||
charactersGrid.appendChild(noResults);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show detailed information for a character
|
||
* @param {string} name - Character name
|
||
*/
|
||
function showCharacterDetails(name) {
|
||
const character = trustData.characters[name];
|
||
|
||
if (!character) {
|
||
characterInfo.innerHTML = '<div class="placeholder-message"><p>Character not found</p></div>';
|
||
return;
|
||
}
|
||
|
||
// Update current character
|
||
currentCharacter = character;
|
||
|
||
// Mark the character card as active
|
||
const cards = document.querySelectorAll('.character-card');
|
||
cards.forEach(card => {
|
||
if (card.dataset.name === name) {
|
||
card.classList.add('active');
|
||
} else {
|
||
card.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Helper function to safely format text content
|
||
const formatText = (text) => {
|
||
if (!text) return '';
|
||
|
||
// Replace HTML entities to prevent XSS
|
||
return text
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
};
|
||
|
||
// Helper function to format grid items (spells, abilities, weapon skills)
|
||
const formatGridItems = (text) => {
|
||
if (!text) return '';
|
||
|
||
// Split by newlines or commas
|
||
const items = text.split(/[\n,]+/).map(item => item.trim()).filter(item => item);
|
||
|
||
if (items.length === 0) return '';
|
||
|
||
return `<div class="grid-items">${
|
||
items.map(item => `<div class="grid-item">${formatText(item)}</div>`).join('')
|
||
}</div>`;
|
||
};
|
||
|
||
// Helper function to format bullet lists (acquisition, special features, trust synergy)
|
||
const formatBulletList = (text) => {
|
||
if (!text) return '';
|
||
|
||
// Split by newlines or periods followed by a space or newline
|
||
const items = text.split(/\.\s+|\n+/).map(item => item.trim()).filter(item => item);
|
||
|
||
if (items.length === 0) return '';
|
||
|
||
return `<ul>${
|
||
items.map(item => `<li>${formatText(item)}</li>`).join('')
|
||
}</ul>`;
|
||
};
|
||
|
||
// Build the HTML for the character details
|
||
let html = `
|
||
<h2>${formatText(character.name)}</h2>
|
||
${character.altName ? `<h3 class="alt-name">Also known as: ${formatText(character.altName)}</h3>` : ''}
|
||
<div class="character-section character-header">
|
||
<div>
|
||
<h3>Role</h3>
|
||
<p>${formatText(character.role)}</p>
|
||
</div>
|
||
<div class="acquired-toggle">
|
||
<label for="acquired-checkbox">Acquired:</label>
|
||
<input type="checkbox" id="acquired-checkbox" ${character.acquired ? 'checked' : ''} data-id="${character.id}">
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add job information if available
|
||
if (character.job) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Job</h3>
|
||
<p>${formatText(character.job)}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add spells if available
|
||
if (character.spells) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Spells</h3>
|
||
${formatGridItems(character.spells)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add abilities if available
|
||
if (character.abilities) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Abilities</h3>
|
||
${formatGridItems(character.abilities)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add weapon skills if available
|
||
if (character.weaponSkills) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Weapon Skills</h3>
|
||
${formatGridItems(character.weaponSkills)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add acquisition information if available
|
||
if (character.acquisition) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Acquisition</h3>
|
||
${formatBulletList(character.acquisition)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add special features if available
|
||
if (character.specialFeatures) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Special Features</h3>
|
||
${formatBulletList(character.specialFeatures)}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add trust synergy if available
|
||
if (character.trustSynergy) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Trust Synergy</h3>
|
||
${formatBulletList(character.trustSynergy)}
|
||
</div>
|
||
`;
|
||
|
||
// Add trust synergy names if available
|
||
if (character.trustSynergyNames && character.trustSynergyNames.length > 0) {
|
||
html += `
|
||
<div class="character-section">
|
||
<h3>Synergy Characters</h3>
|
||
<div class="synergy-characters">
|
||
${character.trustSynergyNames.map(name => {
|
||
const synergyChar = trustData.characters[name];
|
||
const roleClass = synergyChar && roleClasses[synergyChar.role] ? roleClasses[synergyChar.role] : '';
|
||
return `<span class="synergy-character ${roleClass}" data-name="${name}">${name}</span>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Update the character info container
|
||
characterInfo.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* Show only synergy sets in the characters grid
|
||
*/
|
||
function showOnlySynergySets() {
|
||
// Clear the grid
|
||
charactersGrid.innerHTML = '';
|
||
|
||
// Hide the synergy sets container since we're showing them in the main grid
|
||
document.getElementById('synergy-sets-container').style.display = 'none';
|
||
|
||
// Get synergy sets
|
||
const synergySets = identifySynergySets();
|
||
|
||
// If no synergy sets, show a message
|
||
if (synergySets.length === 0) {
|
||
const noResults = document.createElement('div');
|
||
noResults.className = 'placeholder-message';
|
||
noResults.innerHTML = '<p>No synergy sets found</p>';
|
||
charactersGrid.appendChild(noResults);
|
||
return;
|
||
}
|
||
|
||
// Add each synergy set
|
||
synergySets.forEach(set => {
|
||
// Create section header
|
||
const sectionHeader = document.createElement('div');
|
||
sectionHeader.className = 'role-section-header Synergy';
|
||
|
||
// Add expand/collapse icon
|
||
const expandIcon = document.createElement('span');
|
||
expandIcon.className = 'expand-icon';
|
||
expandIcon.textContent = '−'; // Unicode minus sign (expanded by default)
|
||
sectionHeader.appendChild(expandIcon);
|
||
|
||
// Add set name
|
||
const setName = document.createElement('span');
|
||
setName.textContent = set.name;
|
||
sectionHeader.appendChild(setName);
|
||
|
||
// Add character count
|
||
const countSpan = document.createElement('span');
|
||
countSpan.className = 'character-count';
|
||
countSpan.textContent = `(${set.characters.length})`;
|
||
sectionHeader.appendChild(countSpan);
|
||
|
||
// Add section header to grid
|
||
charactersGrid.appendChild(sectionHeader);
|
||
|
||
// Create container for characters in this set
|
||
const charactersContainer = document.createElement('div');
|
||
charactersContainer.className = 'role-characters-container';
|
||
charactersGrid.appendChild(charactersContainer);
|
||
|
||
// Filter characters by view
|
||
let characters = set.characters;
|
||
if (currentView !== 'all') {
|
||
characters = filterCharactersByView(characters);
|
||
}
|
||
|
||
// Skip this set if no characters match the current view
|
||
if (characters.length === 0) {
|
||
// Remove the section header and container
|
||
charactersGrid.removeChild(sectionHeader);
|
||
charactersGrid.removeChild(charactersContainer);
|
||
return;
|
||
}
|
||
|
||
// Update the character count to reflect filtered results
|
||
countSpan.textContent = `(${characters.length})`;
|
||
|
||
// Add characters for this set
|
||
characters.forEach(name => {
|
||
const fullCharacter = trustData.characters[name];
|
||
if (!fullCharacter) return;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'character-card';
|
||
|
||
// Add role class
|
||
if (fullCharacter.role && roleClasses[fullCharacter.role]) {
|
||
card.classList.add(roleClasses[fullCharacter.role]);
|
||
}
|
||
|
||
// Add acquired class if the character is acquired
|
||
if (fullCharacter.acquired) {
|
||
card.classList.add('acquired');
|
||
}
|
||
|
||
card.dataset.name = name;
|
||
card.textContent = name;
|
||
|
||
// If this is the current character, mark it as active
|
||
if (currentCharacter && currentCharacter.name === name) {
|
||
card.classList.add('active');
|
||
}
|
||
|
||
charactersContainer.appendChild(card);
|
||
});
|
||
});
|
||
|
||
// Hide the synergy sets container since we're showing them in the main grid
|
||
document.getElementById('synergy-sets-container').style.display = 'none';
|
||
|
||
currentRoleHeading.textContent = 'Synergy Sets';
|
||
}
|
||
|
||
/**
|
||
* Filter characters based on the current view (all, acquired, unacquired)
|
||
* @param {Array} characters - Array of character objects or names
|
||
* @returns {Array} - Filtered array of character objects
|
||
*/
|
||
function filterCharactersByView(characters) {
|
||
if (currentView === 'all') {
|
||
return characters;
|
||
}
|
||
|
||
return characters.filter(char => {
|
||
const fullCharacter = typeof char === 'string'
|
||
? trustData.characters[char]
|
||
: (char.name ? trustData.characters[char.name] : char);
|
||
|
||
if (!fullCharacter) return false;
|
||
|
||
return currentView === 'acquired' ? fullCharacter.acquired : !fullCharacter.acquired;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Set up event listeners for user interactions
|
||
*/
|
||
function setupEventListeners() {
|
||
// View toggle
|
||
const viewToggleButtons = document.querySelectorAll('.view-toggle-btn');
|
||
viewToggleButtons.forEach(button => {
|
||
button.addEventListener('click', () => {
|
||
// Update active class
|
||
viewToggleButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
|
||
// Update current view
|
||
currentView = button.dataset.view;
|
||
|
||
// Refresh the display
|
||
if (currentRole === 'All') {
|
||
showAllCharacters();
|
||
} else {
|
||
showCharactersByRole(currentRole);
|
||
}
|
||
|
||
// Update synergy sets
|
||
showSynergySets();
|
||
|
||
// Update the heading to reflect the current view
|
||
let viewText = '';
|
||
switch (currentView) {
|
||
case 'acquired':
|
||
viewText = ' (Acquired)';
|
||
break;
|
||
case 'unacquired':
|
||
viewText = ' (Unacquired)';
|
||
break;
|
||
}
|
||
|
||
if (currentRoleHeading.textContent.includes('Search Results')) {
|
||
// Don't modify search results heading
|
||
} else if (currentRole === 'All') {
|
||
currentRoleHeading.textContent = `All Characters${viewText}`;
|
||
} else {
|
||
currentRoleHeading.textContent = `${currentRole} Characters${viewText}`;
|
||
}
|
||
});
|
||
});
|
||
// Role selection
|
||
rolesList.addEventListener('click', event => {
|
||
const li = event.target.closest('li');
|
||
if (!li) return;
|
||
|
||
// Update active class
|
||
document.querySelectorAll('#roles-list li').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
li.classList.add('active');
|
||
|
||
// Show characters for the selected role
|
||
const role = li.dataset.role;
|
||
currentRole = role;
|
||
|
||
if (role === 'All') {
|
||
showAllCharacters();
|
||
} else if (role === 'Synergy') {
|
||
showOnlySynergySets();
|
||
} else {
|
||
showCharactersByRole(role);
|
||
}
|
||
|
||
// Update synergy sets if not showing only synergy sets
|
||
if (role !== 'Synergy') {
|
||
showSynergySets();
|
||
}
|
||
});
|
||
|
||
// Character selection
|
||
charactersGrid.addEventListener('click', event => {
|
||
const card = event.target.closest('.character-card');
|
||
if (!card) return;
|
||
|
||
const name = card.dataset.name;
|
||
showCharacterDetails(name);
|
||
});
|
||
|
||
// Search
|
||
searchInput.addEventListener('input', event => {
|
||
const query = event.target.value.trim();
|
||
|
||
if (query === '') {
|
||
// If search is cleared, show characters for the current role
|
||
if (currentRole === 'All') {
|
||
showAllCharacters();
|
||
} else {
|
||
showCharactersByRole(currentRole);
|
||
}
|
||
} else {
|
||
// Search for characters and filter by view
|
||
let results = trustParser.searchCharacters(query);
|
||
|
||
// Only filter by view if not showing all characters
|
||
if (currentView !== 'all') {
|
||
results = filterCharactersByView(results);
|
||
currentRoleHeading.textContent = `Search Results: "${query}" (${currentView === 'acquired' ? 'Acquired' : 'Unacquired'})`;
|
||
} else {
|
||
currentRoleHeading.textContent = `Search Results: "${query}"`;
|
||
}
|
||
|
||
showCharacters(results);
|
||
|
||
// Update synergy sets
|
||
showSynergySets();
|
||
}
|
||
});
|
||
|
||
// Synergy character click in character details
|
||
characterInfo.addEventListener('click', event => {
|
||
const synergyChar = event.target.closest('.synergy-character');
|
||
if (synergyChar) {
|
||
const name = synergyChar.dataset.name;
|
||
if (name) {
|
||
showCharacterDetails(name);
|
||
|
||
// Scroll to the top of the character details
|
||
characterInfo.scrollTop = 0;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Synergy set character click
|
||
document.getElementById('synergy-sets').addEventListener('click', event => {
|
||
const synergyChar = event.target.closest('.synergy-set-character');
|
||
if (synergyChar) {
|
||
const name = synergyChar.dataset.name;
|
||
if (name) {
|
||
showCharacterDetails(name);
|
||
|
||
// Scroll to the top of the character details
|
||
characterInfo.scrollTop = 0;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Acquired checkbox toggle
|
||
characterInfo.addEventListener('change', async event => {
|
||
if (event.target.id === 'acquired-checkbox') {
|
||
const id = parseInt(event.target.dataset.id);
|
||
|
||
try {
|
||
// Show loading state
|
||
event.target.disabled = true;
|
||
|
||
// Toggle the acquired status
|
||
const updatedChar = await trustParser.toggleAcquired(id);
|
||
|
||
// Update the checkbox
|
||
event.target.checked = updatedChar.acquired;
|
||
|
||
// Update the character card if it has an acquired class
|
||
const cards = document.querySelectorAll('.character-card');
|
||
cards.forEach(card => {
|
||
if (card.dataset.name === updatedChar.name) {
|
||
if (updatedChar.acquired) {
|
||
card.classList.add('acquired');
|
||
} else {
|
||
card.classList.remove('acquired');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update synergy set characters
|
||
const synergySetChars = document.querySelectorAll('.synergy-set-character');
|
||
synergySetChars.forEach(char => {
|
||
if (char.dataset.name === updatedChar.name) {
|
||
if (updatedChar.acquired) {
|
||
char.classList.add('acquired');
|
||
} else {
|
||
char.classList.remove('acquired');
|
||
}
|
||
}
|
||
});
|
||
|
||
// If we're in a filtered view, refresh the display
|
||
if (currentView !== 'all') {
|
||
if (currentRole === 'All') {
|
||
showAllCharacters();
|
||
} else {
|
||
showCharactersByRole(currentRole);
|
||
}
|
||
|
||
// Update synergy sets
|
||
showSynergySets();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error toggling acquired status:', error);
|
||
// Revert the checkbox state
|
||
event.target.checked = !event.target.checked;
|
||
alert('Failed to update acquired status. Please try again.');
|
||
} finally {
|
||
// Re-enable the checkbox
|
||
event.target.disabled = false;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Collapsible section headers
|
||
charactersGrid.addEventListener('click', event => {
|
||
const header = event.target.closest('.role-section-header');
|
||
if (!header) return;
|
||
|
||
// Toggle the collapsed state
|
||
const container = header.nextElementSibling;
|
||
if (container && container.classList.contains('role-characters-container')) {
|
||
container.classList.toggle('collapsed');
|
||
|
||
// Update the expand/collapse icon
|
||
const icon = header.querySelector('.expand-icon');
|
||
if (icon) {
|
||
if (container.classList.contains('collapsed')) {
|
||
icon.textContent = '+'; // Plus sign for collapsed
|
||
} else {
|
||
icon.textContent = '−'; // Minus sign for expanded
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize the application
|
||
init();
|
||
});
|