${escapeHtml(c.name)}
${c.epithet ? '"' + escapeHtml(c.epithet) + '"' : ' '}
${c.bounty ? formatBounty(c.bounty) : 'Bounty Unknown'}
/* 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 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 `
${c.epithet ? '"' + escapeHtml(c.epithet) + '"' : ' '} ${c.bounty ? formatBounty(c.bounty) : 'Bounty Unknown'} ${escapeHtml(f.tagline)} ${ownerChar ? `${owner.status.replace(/former|current/i, (m) => m[0].toUpperCase() + m.slice(1))}: ${escapeHtml(ownerChar.name)}` : 'No known user'}${escapeHtml(c.name)}
${escapeHtml(f.name)}
${escapeHtml(f.category)}${f.subtype ? ' · ' + escapeHtml(f.subtype.split(' —')[0].split(' (')[0]) : ''}