Files
ffxi-trusts/app.js
2025-07-12 23:10:19 +01:00

1099 lines
41 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
// 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();
});