/* One Piece Wiki — shared utilities used by every page. */ const FACTION_COLORS = [ [/straw hat/i, '#e63946'], [/whitebeard/i, '#6fb7e8'], [/blackbeard/i, '#7a5ea8'], [/big mom/i, '#e0559a'], [/beast pirates/i, '#2f8f7a'], [/kozuki|wano/i, '#b3372c'], [/revolutionary/i, '#3fa34d'], [/donquixote/i, '#e08a2b'], [/roger pirates/i, '#d4af37'], [/heart pirates/i, '#f2c230'], [/kid pirates/i, '#b23a48'], [/cp0|cp9|world government/i, '#8a93a6'], [/marine/i, '#4c7fc0'], [/warlord|cross guild/i, '#9c7a3c'], [/germa/i, '#2f96a3'], [/fishman|ryugu|amazon lily|kuja/i, '#2aa38f'], ]; function hashHue(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return Math.abs(hash) % 360; } function accentFor(entity) { const key = (entity.affiliation || entity.name || '').toString(); for (const [pattern, color] of FACTION_COLORS) { if (pattern.test(key)) return color; } return `hsl(${hashHue(entity.name || key)}, 58%, 48%)`; } function initialFor(name) { const match = (name || '?').match(/[A-Za-z]/); return match ? match[0].toUpperCase() : '?'; } function formatBounty(amount) { if (amount === null || amount === undefined) return 'Unknown'; return '฿' + amount.toLocaleString('en-US'); } function escapeHtml(str) { return String(str ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function qs(name) { return new URLSearchParams(window.location.search).get(name); } function findCharacter(id) { return CHARACTERS.find((c) => c.id === id) || null; } function findFruit(id) { return FRUITS.find((f) => f.id === id) || null; } function characterLink(id) { return `character.html?id=${encodeURIComponent(id)}`; } function fruitLink(id) { return `fruit.html?id=${encodeURIComponent(id)}`; } function statusBadgeClass(status) { if (!status) return 'badge--unknown'; if (/alive/i.test(status)) return 'badge--alive'; if (/deceased/i.test(status)) return 'badge--deceased'; return 'badge--unknown'; } function characterCardHTML(c) { const accent = accentFor(c); const deadRibbon = /deceased/i.test(c.status || '') ? 'Deceased' : ''; const fruitTag = c.devilFruit ? 'Devil Fruit' : ''; return `
${deadRibbon} WANTED
${initialFor(c.name)}

${escapeHtml(c.name)}

${c.epithet ? '"' + escapeHtml(c.epithet) + '"' : ' '}

${c.bounty ? formatBounty(c.bounty) : 'Bounty Unknown'}

${escapeHtml(c.affiliation)} ${fruitTag}
`; } function fruitCardHTML(f) { const owner = f.users.find((u) => /current/i.test(u.status)) || f.users[0]; const ownerChar = owner ? findCharacter(owner.id) : null; return `
${f.icon}

${escapeHtml(f.name)}

${escapeHtml(f.category)}${f.subtype ? ' · ' + escapeHtml(f.subtype.split(' —')[0].split(' (')[0]) : ''}

${escapeHtml(f.tagline)}

${ownerChar ? `${owner.status.replace(/former|current/i, (m) => m[0].toUpperCase() + m.slice(1))}: ${escapeHtml(ownerChar.name)}` : 'No known user'}

`; } function renderCardGrid(container, items, renderFn, emptyMessage) { if (!items.length) { container.innerHTML = `
${escapeHtml(emptyMessage || 'No results found.')}
`; return; } container.innerHTML = items.map(renderFn).join(''); } function markActiveNav() { const page = window.location.pathname.split('/').pop() || 'index.html'; document.querySelectorAll('.site-nav a').forEach((a) => { const href = a.getAttribute('href'); const isCharacters = page === 'index.html' && href === 'index.html'; const isFruits = (page === 'fruits.html' || page === 'fruit.html') && href === 'fruits.html'; const isCharDetail = page === 'character.html' && href === 'index.html'; if (isCharacters || isFruits || isCharDetail) a.classList.add('is-active'); }); } document.addEventListener('DOMContentLoaded', markActiveNav);