(function () { const CLASSES = [ { name: 'Warrior', fr: 'Guerrier', logo: 'War.png', color: '#C79C6E' }, { name: 'Paladin', fr: 'Paladin', logo: 'Pal.png', color: '#F58CBA' }, { name: 'Hunter', fr: 'Chasseur', logo: 'Hunt.png', color: '#ABD473' }, { name: 'Rogue', fr: 'Voleur', logo: 'Rogue.png', color: '#FFF569' }, { name: 'Priest', fr: 'Prêtre', logo: 'Priest.png', color: '#FFFFFF' }, { name: 'Death Knight', fr: 'Chevalier de la mort', logo: 'DK.png', color: '#C41F3B' }, { name: 'Shaman', fr: 'Chaman', logo: 'Sham.png', color: '#0070DE' }, { name: 'Mage', fr: 'Mage', logo: 'Mage.png', color: '#40C7EB' }, { name: 'Warlock', fr: 'Démoniste', logo: 'Lock.png', color: '#8787ED' }, { name: 'Monk', fr: 'Moine', logo: 'monk.png', color: '#00FF96' }, { name: 'Druid', fr: 'Druide', logo: 'Druid.png', color: '#FF7D0A' }, { name: 'Demon Hunter', fr: 'Chasseur de démons', logo: 'dh.png', color: '#A330C9' }, { name: 'Evoker', fr: 'Évocateur', logo: 'evoker.png', color: '#33937F' } ]; const CLASS_BY_NAME = Object.fromEntries(CLASSES.map((c) => [c.name, c])); const classInfo = (name) => CLASS_BY_NAME[name] || { name: name || '?', fr: name || 'Inconnu', logo: null, color: '#8b94a7' }; const SPECS_FR = { 'Elemental': 'Élémentaire', 'Enhancement': 'Amélioration', 'Restoration': 'Restauration', 'Holy': 'Sacré', 'Protection': 'Protection', 'Retribution': 'Rétribution', 'Discipline': 'Discipline', 'Shadow': 'Ombre', 'Blood': 'Sang', 'Frost': 'Givre', 'Unholy': 'Impiété', 'Havoc': 'Dévastation', 'Vengeance': 'Vengeance', 'Devastation': 'Dévastation', 'Preservation': 'Préservation', 'Augmentation': 'Augmentation', 'Arcane': 'Arcane', 'Fire': 'Feu', 'Assassination': 'Assassinat', 'Outlaw': 'Hors-la-loi', 'Subtlety': 'Subtilité', 'Beast Mastery': 'Maîtrise des bêtes', 'Marksmanship': 'Précision', 'Survival': 'Survie', 'Arms': 'Armes', 'Fury': 'Fureur', 'Balance': 'Équilibre', 'Feral': 'Farouche', 'Guardian': 'Gardien', 'Brewmaster': 'Maître brasseur', 'Mistweaver': 'Tisse-brume', 'Windwalker': 'Marche-vent', 'Affliction': 'Affliction', 'Demonology': 'Démonologie', 'Destruction': 'Destruction' }; const specFr = (s) => (s ? (SPECS_FR[s] || s) : null); const SPECS_BY_CLASS = { 'Death Knight': [ { key: 'Blood', fr: 'Sang', icon: 'spell_deathknight_bloodpresence.jpg' }, { key: 'Frost', fr: 'Givre', icon: 'spell_deathknight_frostpresence.jpg' }, { key: 'Unholy', fr: 'Impie', icon: 'spell_deathknight_unholypresence.jpg' } ], 'Demon Hunter': [ { key: 'Havoc', fr: 'Dévastation', icon: 'ability_demonhunter_specdps.jpg' }, { key: 'Vengeance', fr: 'Vengeance', icon: 'ability_demonhunter_spectank.jpg' }, { key: 'Devoration', fr: 'Dévoration', icon: 'classicon_demonhunter_void.jpg' } ], 'Druid': [ { key: 'Balance', fr: 'Équilibre', icon: 'spell_nature_starfall.jpg' }, { key: 'Feral', fr: 'Farouche', icon: 'ability_druid_catform.jpg' }, { key: 'Guardian', fr: 'Gardien', icon: 'ability_racial_bearform.jpg' }, { key: 'Restoration', fr: 'Restauration', icon: 'spell_nature_healingtouch.jpg' } ], 'Evoker': [ { key: 'Devastation', fr: 'Dévastation', icon: 'classicon_evoker_devastation.jpg' }, { key: 'Preservation', fr: 'Préservation', icon: 'classicon_evoker_preservation.jpg' }, { key: 'Augmentation', fr: 'Augmentation', icon: 'classicon_evoker_augmentation.jpg' } ], 'Hunter': [ { key: 'Beast Mastery', fr: 'Maîtrise des bêtes', icon: 'ability_hunter_bestialdiscipline.jpg' }, { key: 'Marksmanship', fr: 'Précision', icon: 'ability_hunter_focusedaim.jpg' }, { key: 'Survival', fr: 'Survie', icon: 'ability_hunter_camouflage.jpg' } ], 'Mage': [ { key: 'Arcane', fr: 'Arcanes', icon: 'spell_holy_magicalsentry.jpg' }, { key: 'Fire', fr: 'Feu', icon: 'spell_fire_firebolt02.jpg' }, { key: 'Frost', fr: 'Givre', icon: 'spell_frost_frostbolt02.jpg' } ], 'Monk': [ { key: 'Brewmaster', fr: 'Maître brasseur', icon: 'spell_monk_brewmaster_spec.jpg' }, { key: 'Mistweaver', fr: 'Tisse-brume', icon: 'spell_monk_mistweaver_spec.jpg' }, { key: 'Windwalker', fr: 'Marche-vent', icon: 'spell_monk_windwalker_spec.jpg' } ], 'Paladin': [ { key: 'Holy', fr: 'Sacré', icon: 'spell_holy_holybolt.jpg' }, { key: 'Protection', fr: 'Protection', icon: 'ability_paladin_shieldofthetemplar.jpg' }, { key: 'Retribution', fr: 'Vindicte', icon: 'spell_holy_auraoflight.jpg' } ], 'Priest': [ { key: 'Discipline', fr: 'Discipline', icon: 'spell_holy_powerwordshield.jpg' }, { key: 'Holy', fr: 'Sacré', icon: 'spell_holy_guardianspirit.jpg' }, { key: 'Shadow', fr: 'Ombre', icon: 'spell_shadow_shadowwordpain.jpg' } ], 'Rogue': [ { key: 'Assassination', fr: 'Assassinat', icon: 'ability_rogue_deadlybrew.jpg' }, { key: 'Outlaw', fr: 'Hors-la-loi', icon: 'ability_rogue_waylay.jpg' }, { key: 'Subtlety', fr: 'Finesse', icon: 'ability_stealth.jpg' } ], 'Shaman': [ { key: 'Elemental', fr: 'Élémentaire', icon: 'spell_nature_lightning.jpg' }, { key: 'Enhancement', fr: 'Amélioration', icon: 'spell_shaman_improvedstormstrike.jpg' }, { key: 'Restoration', fr: 'Restauration', icon: 'spell_nature_magicimmunity.jpg' } ], 'Warlock': [ { key: 'Affliction', fr: 'Affliction', icon: 'spell_shadow_deathcoil.jpg' }, { key: 'Demonology', fr: 'Démonologie', icon: 'spell_shadow_metamorphosis.jpg' }, { key: 'Destruction', fr: 'Destruction', icon: 'spell_shadow_rainoffire.jpg' } ], 'Warrior': [ { key: 'Arms', fr: 'Armes', icon: 'ability_warrior_innerrage.jpg' }, { key: 'Fury', fr: 'Fureur', icon: 'ability_warrior_savageblow.jpg' }, { key: 'Protection', fr: 'Protection', icon: 'ability_warrior_defensivestance.jpg' } ] }; const $ = (sel) => document.querySelector(sel); const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); let classGroups = new Map(); let classTitles = { glad: [], legend: [] }; function fmt(n) { if (n === null || n === undefined) return '—'; return n.toLocaleString('fr-FR'); } function fmtSet(v) { if (v === null || v === undefined || v === '' || v === 0) return '—'; return String(v); } function pct(n, d) { return d > 0 ? Math.max(0, Math.min(100, Math.round((n / d) * 100))) : 0; } function charTopRating(c) { let best = 0; const pm = c.pvp_manual || {}; for (const k of ['ARENA_2v2', 'ARENA_3v3', 'RATED_BG']) { const m = pm[k] || {}; const v = Math.max(m.max || 0, m.season || 0); if (v > best) best = v; } const specs = pm.specs || {}; for (const k of Object.keys(specs)) { for (const br of ['SOLO_SHUFFLE', 'BLITZ']) { const m = specs[k][br] || {}; const v = Math.max(m.max || 0, m.season || 0); if (v > best) best = v; } } if (best) return best; const auto = (c.data && (c.data.blizzard || c.data.raiderio || {}).pvp) || {}; return Math.max(0, ...Object.values(auto).map((x) => (x && typeof x.rating === 'number' ? x.rating : 0))); } function armoryImage(thumbnail) { if (!thumbnail) return null; return thumbnail.replace(/-avatar\.jpg/i, '-inset.jpg'); } function overviewBadges(classes) { const covered = new Set(classes || []); const items = CLASSES.map((cls) => { const done = covered.has(cls.name); const img = cls.logo ? `${esc(cls.fr)}` : `${esc(cls.fr[0])}`; return `${img}${done ? '✓' : ''}`; }).join(''); return `
${items}
`; } const BEST_ORDER = [['ARENA_2v2', '2v2'], ['ARENA_3v3', '3v3'], ['SOLO_SHUFFLE', 'Solo Shuffle'], ['BLITZ', 'Blitz'], ['RATED_BG', 'RBG']]; const BEST_COLOR = { ARENA_2v2: '#5aa9ff', ARENA_3v3: '#ff5c5c', SOLO_SHUFFLE: '#37e0ff', BLITZ: '#ffb347', RATED_BG: '#7ed957' }; function bestsTable(bests) { const rows = BEST_ORDER.map(([key, label]) => { const b = bests[key] || null; if (!b) { return `
${label}
—
`; } const info = classInfo(b.class); const spec = b.spec ? specFr(b.spec) : null; const specEntry = (SPECS_BY_CLASS[info.name] || []).find((s) => s.key === b.spec); const avatar = specEntry && specEntry.icon ? `` : (info.logo ? `` : ''); const tier = tierFor(b.rating); return `
${label} ${tier ? `${tier.name}` : ''}
${avatar}
${esc(info.fr)}${spec ? ` · ${esc(spec)}` : ''}
${esc(b.name || '')}
${fmt(b.rating)}
`; }).join(''); return `

Meilleure classe & spé par bracket

${rows}
`; } function overviewCard(g) { const manualBadge = g.manual && g.key !== 'achievements' ? 'manuel' : ''; const done = !!g.achieved; const doneBadge = done ? '✓ Atteint' : ''; let value; let sub; let progress = 0; let badges = ''; if (g.key === 'class90' || g.key === 'ss2300') { value = `${fmt(g.covered)} / ${g.total} classes`; sub = g.total > 0 ? `${pct(g.covered, g.total)}%` : '—'; progress = g.total > 0 ? g.covered / g.total : 0; badges = overviewBadges(g.classes); } else if (g.key === 'gladiator_s2') { const w = g.wins || 0; const t = g.total || 50; value = g.obtained ? 'Obtenu' : `${fmt(w)} / ${fmt(t)} victoires`; sub = ''; progress = t > 0 ? Math.min(1, w / t) : 0; badges = overviewBadges(g.classes || []); } else if (g.key === 'achievements') { const pc = g.total > 0 ? pct(g.points, g.total) : 0; value = `${fmt(g.points)} / ${fmt(g.total)}`; sub = `${pc}% · Points de hauts faits`; progress = g.total > 0 ? Math.min(1, g.points / g.total) : 0; } return `

${esc(g.title)}

${manualBadge} ${doneBadge}
${value}
${sub ? `
${esc(sub)}
` : ''}
${badges}
`; } const TIERS = [ { min: 2400, name: 'Elite', color: '#ff8000' }, { min: 2100, name: 'Duelist', color: '#a335ee' }, { min: 1800, name: 'Rival', color: '#0070dd' }, { min: 1400, name: 'Challenger', color: '#1eff00' }, { min: 0, name: 'Combatant', color: '#9d9d9d' } ]; const tierFor = (rating) => { if (!rating || rating <= 0) return null; return TIERS.find((t) => rating >= t.min) || null; }; const brkBlock = (label, color, b) => { const current = b.season || null; const max = b.max || null; const tier = tierFor(current); const total = (b.won || 0) + (b.lost || 0); const wr = total > 0 ? Math.round((b.won / total) * 100) : null; return `
${label} ${tier ? `${tier.name}` : 'Non classé'} ${b.isRecord ? '★ Record' : ''}
${current ? fmt(current) : '—'} ${max != null ? `/${fmt(max)}` : ''}
${current != null ? `Côte actuelle · ` : ''}${max != null ? `Max XP ${fmt(max)}` : (current != null ? 'Aucun max enregistré' : 'Aucune côte')}
${fmt(b.won)}V · ${fmt(b.lost)}D${wr != null ? ` ${wr}%` : ''}
${total ? `${fmt(total)} games` : 'Aucune game cette saison'}
`; }; function bracketValues(c) { const pm = c.pvp_manual || {}; const auto = c.pvp || {}; const autoOf = (key) => (auto[key] && typeof auto[key].rating === 'number' ? auto[key] : null); const resolve = (key, m) => { const a = autoOf(key) || {}; const season = m.season || (a.rating || null); const max = m.max || (c.pvp_best && c.pvp_best[key]) || null; return { season, max, won: m.won || a.won || 0, lost: m.lost || a.lost || 0, weekly_won: a.weekly_won || 0, weekly_lost: a.weekly_lost || 0, tier: a.tier || null, isRecord: !!(c.pvp_records && c.pvp_records[key]) }; }; return { generalVal: (key) => resolve(key, pm[key] || {}), specVal: (spec, key) => { const s = (pm.specs && pm.specs[spec]) || {}; return resolve(key, s[key] || {}); } }; } function bracketsGrid(c, spec) { const { generalVal, specVal } = bracketValues(c); const PANELS = [ ['2v2', '#5aa9ff', generalVal('ARENA_2v2')], ['3v3', '#ff5c5c', generalVal('ARENA_3v3')], ['Solo', '#37e0ff', spec ? specVal(spec, 'SOLO_SHUFFLE') : null], ['Blitz', '#ffb347', spec ? specVal(spec, 'BLITZ') : null], ['RBG', '#7ed957', generalVal('RATED_BG')] ]; return `
${PANELS.map(([label, color, b]) => brkBlock(label, color, b)).join('')}
`; } let modalChars = []; let modalSpecByChar = {}; const fullCache = {}; function getFull(id) { if (!fullCache[id]) fullCache[id] = api.get(`/api/characters/${id}`); return fullCache[id]; } function charRow(c) { const info = classInfo(c.class); const frozen = (c.media && (c.media.main || c.media.inset || c.media.avatar)) || renderImage(c.thumbnail_url) || c.thumbnail_url || null; const ilvl = c.ilvl || null; const specs = SPECS_BY_CLASS[info.name] || []; const sel = modalSpecByChar[c.id] || null; const specPanels = specs.map((s) => ` `).join(''); const metaBits = []; if (c.realm) metaBits.push(esc(c.realm)); if (c.race) metaBits.push(esc(c.race)); const faction = c.faction === 'ALLIANCE' || c.faction === 'Alliance' ? 'Alliance' : c.faction === 'HORDE' || c.faction === 'Horde' ? 'Horde' : null; if (faction) metaBits.push(faction); const metaText = metaBits.filter(Boolean).join(' · '); let detail = ''; if (sel) { detail = `
${bracketsGrid(c, sel)}
`; } return `
${specPanels}
${c.check_pvp_url ? `${esc(c.name)}` : esc(c.name)}
${metaText}${ilvl != null ? `ILVL ${fmt(ilvl)}` : ''}
${classTitleEntries(info, classTitles)}
${detail}
`; } function titleTags(cls, titles) { const parts = []; if ((titles.glad || []).includes(cls.name)) parts.push('Gladiateur'); if ((titles.legend || []).includes(cls.name)) parts.push('Légende'); if (!parts.length) return ''; return `${parts.join('')}`; } function classTitleEntries(cls, titles) { const items = []; for (const e of (titles.glad_entries || [])) { if (e.class === cls.name) items.push(`GladiateurGladiateur${e.season ? esc(e.season) : ''}`); } for (const e of (titles.legend_entries || [])) { if (e.class === cls.name) items.push(`LégendeLégende${e.season ? esc(e.season) : ''}`); } if (!items.length) { if ((titles.glad || []).includes(cls.name)) items.push(`GladiateurGladiateur${titles.glad_season ? esc(titles.glad_season) : ''}`); if ((titles.legend || []).includes(cls.name)) items.push(`LégendeLégende${titles.legend_season ? esc(titles.legend_season) : ''}`); } return items.join(''); } function renderModalChars() { if (!modalChars.length) { $('#class-modal-chars').innerHTML = '
Aucun personnage pour cette classe
'; return; } $('#class-modal-chars').innerHTML = modalChars.map((c) => charRow(c)).join(''); for (const c of modalChars) { const sel = modalSpecByChar[c.id]; if (!sel) continue; const gearEl = document.querySelector(`[data-gear="${c.id}"]`); const talentsEl = document.querySelector(`[data-talents="${c.id}"]`); if (gearEl) { getFull(c.id).then((full) => { if (gearEl) gearEl.innerHTML = gearHTML(full.gear); }).catch(() => { if (gearEl) gearEl.innerHTML = '
Équipement indisponible
'; }); } if (talentsEl) { talentsEl.innerHTML = '
Chargement…
'; api.get(`/api/characters/${c.id}/talents?spec=${encodeURIComponent(sel)}`).then((t) => { if (talentsEl) talentsEl.innerHTML = talentTreeHTML(t); }).catch(() => { if (talentsEl) talentsEl.innerHTML = '
Détails indisponibles
'; }); } } } function openClassModal(cls, chars, titles) { modalChars = chars; modalSpecByChar = {}; $('#class-modal-logo').src = cls.logo ? `/images/classes/${cls.logo}` : ''; $('#class-modal-logo').style.display = cls.logo ? '' : 'none'; renderModalChars(); $('#class-modal-backdrop').hidden = false; document.body.style.overflow = 'hidden'; } $('#class-modal-chars').addEventListener('click', (e) => { const panel = e.target.closest('.wm-spec-panel'); if (!panel) return; const id = panel.dataset.char; const spec = panel.dataset.spec; modalSpecByChar[id] = modalSpecByChar[id] === spec ? null : spec; renderModalChars(); const selPanel = $('#class-modal-chars').querySelector(`.wm-spec-panel.on[data-char="${CSS.escape(id)}"]`); if (selPanel) { selPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); const detail = selPanel.closest('.class-char').querySelector('.wm-detail'); if (detail) detail.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); function closeClassModal() { $('#class-modal-backdrop').hidden = true; document.body.style.overflow = ''; } async function load() { const statusEl = $('#header-status'); const overviewEl = $('#overview-grid'); try { const [overview, chars] = await Promise.all([api.get('/api/overview'), api.get('/api/characters')]); const cards = [ { key: 'class90', title: 'Classes niveau 90', covered: overview.class90.covered, total: overview.class90.total, manual: overview.class90.manual, classes: overview.class90.classes, achieved: overview.class90.total > 0 && overview.class90.covered >= overview.class90.total }, { key: 'ss2300', title: 'Toutes les classes à 2300 en soloshuffle — Saison 2', covered: overview.ss2300.covered, total: overview.ss2300.total, manual: overview.ss2300.manual, classes: overview.ss2300.classes, achieved: overview.ss2300.total > 0 && overview.ss2300.covered >= overview.ss2300.total }, { key: 'gladiator_s2', title: 'Gladiateur S2 Midnight', obtained: overview.gladiator_s2.obtained, wins: overview.gladiator_s2.wins, total: overview.gladiator_s2.total, rating: overview.gladiator_s2.rating, class: overview.gladiator_s2.class, classes: (overview.titles && overview.titles.glad_s2) || [], achieved: !!overview.gladiator_s2.obtained } ]; overviewEl.innerHTML = cards.map(overviewCard).join('') + bestsTable(overview.bests || {}); const grouped = new Map(); for (const c of chars) { const key = classInfo(c.class).name; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(c); } for (const list of grouped.values()) { list.sort((a, b) => charTopRating(b) - charTopRating(a)); } classGroups = grouped; classTitles = overview.titles || { glad: [], legend: [] }; const headerClasses = $('#header-classes'); if (headerClasses) { const class90 = new Set(overview.class90.classes || []); headerClasses.innerHTML = CLASSES.map((cls) => ` `).join(''); } const twitchUrl = overview.twitch.url || 'https://twitch.tv/wycz'; updateLiveStatus(statusEl, twitchUrl); renderSocials($('#socials'), { youtube: (overview.youtube && overview.youtube.url) || '', tiktok: (overview.tiktok && overview.tiktok.url) || '' }); } catch (e) { overviewEl.innerHTML = `
Erreur de chargement : ${esc(e.message)}
`; } } async function updateLiveStatus(statusEl, twitchUrl) { const channel = twitchUrl.replace(/\/+$/, '').split('/').pop() || 'wycz'; const safeChannel = encodeURIComponent(channel); const link = ``; const twitchLogo = `${SOCIAL_SVG.twitch}`; try { const res = await fetch(`https://decapi.me/twitch/uptime/${safeChannel}`, { mode: 'cors' }); const text = (await res.text()).trim(); const isLive = text && !/offline|hors ligne|hors-ligne|not live/i.test(text); statusEl.innerHTML = `${link}${twitchLogo}`; } catch { statusEl.innerHTML = `${link}${twitchLogo}`; } } const SOCIAL_SVG = { twitch: '', youtube: '', tiktok: '' }; function renderSocials(el, socials) { const buttons = []; if (socials.youtube) { buttons.push(`${SOCIAL_SVG.youtube}`); } if (socials.tiktok) { buttons.push(`${SOCIAL_SVG.tiktok}`); } el.innerHTML = buttons.join(''); } const backdrop = $('#modal-backdrop'); const modalArt = $('#modal-art'); const modalError = $('#modal-error'); function closeModal() { backdrop.hidden = true; document.body.style.overflow = ''; } document.addEventListener('click', (e) => { const copyBtn = e.target.closest('[data-copy]'); if (copyBtn) { navigator.clipboard.writeText(copyBtn.dataset.copy || '').then(() => { copyBtn.textContent = 'Copié !'; setTimeout(() => { copyBtn.textContent = 'Copier le build'; }, 1600); }).catch(() => {}); return; } const classIcon = e.target.closest('.class-icon'); if (classIcon) { const name = classIcon.dataset.class; openClassModal(classInfo(name), classGroups.get(name) || [], classTitles); return; } if (e.target === $('#class-modal-backdrop')) { closeClassModal(); return; } const openBtn = e.target.closest('[data-open3d]'); if (openBtn) { if (e.target.closest('a')) return; const id = openBtn.dataset.open3d; modalError.hidden = true; modalArt.style.display = 'flex'; modalArt.innerHTML = '
Chargement…
'; $('#modal-stats').innerHTML = ''; $('#modal-title').textContent = '…'; $('#modal-subtitle').textContent = ''; backdrop.hidden = false; document.body.style.overflow = 'hidden'; api.get(`/api/characters/${id}`).then((full) => { $('#modal-title').textContent = full.name; const sub = []; if (full.spec) sub.push(specFr(full.spec)); if (full.race) sub.push(full.race); if (full.faction) sub.push(full.faction); sub.push(full.realm); $('#modal-subtitle').textContent = sub.filter(Boolean).join(' · '); const thumb = (full.media && full.media.main) || full.media?.avatar || full.thumbnail_url || null; const img = full.media && full.media.main ? thumb : renderImage(thumb); if (img) { modalArt.innerHTML = ``; } else { modalArt.innerHTML = '
Image de l\'armurerie non disponible
'; } renderStats(full); $('#modal-gear').innerHTML = gearHTML(full.gear); if (full.spec) { $('#modal-talents').innerHTML = '
Chargement…
'; api.get(`/api/characters/${id}/talents?spec=${encodeURIComponent(full.spec)}`).then((t) => { $('#modal-talents').innerHTML = talentTreeHTML(t); }).catch(() => { $('#modal-talents').innerHTML = talentHTML(full); }); } else { $('#modal-talents').innerHTML = talentHTML(full); } }).catch((err) => { modalArt.innerHTML = ''; modalError.textContent = `Impossible d'afficher les détails : ${err.message}`; modalError.hidden = false; }); return; } if (e.target === backdrop) closeModal(); }); function renderImage(thumbnail) { if (!thumbnail) return null; return thumbnail.split('?')[0].replace(/-avatar\.jpg/i, '-main-raw.png'); } const GEAR_ORDER = [ ['head', 'Tête'], ['neck', 'Cou'], ['shoulder', 'Épaules'], ['back', 'Dos'], ['chest', 'Torse'], ['wrist', 'Poignets'], ['hands', 'Mains'], ['waist', 'Taille'], ['legs', 'Jambes'], ['feet', 'Pieds'], ['finger1', 'Doigt 1'], ['finger2', 'Doigt 2'], ['trinket1', 'Bijou 1'], ['trinket2', 'Bijou 2'], ['mainhand', 'Main droite'], ['offhand', 'Main gauche'] ]; const QUALITY_COLOR = ['#9d9d9d', '#ffffff', '#1eff00', '#0070dd', '#a335ee', '#ff8000', '#e6cc80']; function gearHTML(gear) { if (!gear || !gear.length) return '
Aucun équipement disponible (données Raider.io)
'; const bySlot = {}; for (const it of gear) bySlot[it.slot_name] = it; const items = GEAR_ORDER.map(([slot, label]) => bySlot[slot] || { slot, slot_name: slot, label, empty: true }); const main = (it) => it.empty ? ` ${esc((it.label || it.slot_name || '?')[0])} ${esc(it.label || it.slot_name)}Vide ` : ` ${it.icon ? `` : `${esc((it.name || '?')[0])}`} ${it.item_id ? `${esc(it.name)}` : `${esc(it.name)}`} ${it.item_level ? `ILVL ${it.item_level}` : ''} `; return items.map(main).join(''); } const TALENT_BG = { 'Warrior': { Arms: 'warrior-arms', Fury: 'warrior-fury', Protection: 'warrior-protection' }, 'Paladin': { Holy: 'paladin-holy', Protection: 'paladin-protection', Retribution: 'paladin-retribution' }, 'Hunter': { 'Beast Mastery': 'hunter-beastmastery', Marksmanship: 'hunter-marksmanship', Survival: 'hunter-survival' }, 'Rogue': { Assassination: 'rogue-assassination', Outlaw: 'rogue-outlaw', Subtlety: 'rogue-subtlety' }, 'Priest': { Discipline: 'priest-discipline', Holy: 'priest-holy', Shadow: 'priest-shadow' }, 'Death Knight': { Blood: 'death-knight-blood', Frost: 'death-knight-frost', Unholy: 'death-knight-unholy' }, 'Shaman': { Elemental: 'shaman-elemental', Enhancement: 'shaman-enhancement', Restoration: 'shaman-restoration' }, 'Mage': { Arcane: 'mage-arcane', Fire: 'mage-fire', Frost: 'mage-frost' }, 'Warlock': { Affliction: 'warlock-affliction', Demonology: 'warlock-demonology', Destruction: 'warlock-destruction' }, 'Monk': { Brewmaster: 'monk-brewmaster', Mistweaver: 'monk-mistweaver', Windwalker: 'monk-windwalker' }, 'Druid': { Balance: 'druid-balance', Feral: 'druid-feral', Guardian: 'druid-guardian', Restoration: 'druid-restoration' }, 'Demon Hunter': { Havoc: 'demon-hunter-havoc', Vengeance: 'demon-hunter-vengeance' }, 'Evoker': { Devastation: 'evoker-devastation', Preservation: 'evoker-preservation', Augmentation: 'evoker-augmentation' } }; function talentHTML(full) { const cls = full.class || ''; const spec = full.spec || ''; const bg = TALENT_BG[cls] && TALENT_BG[cls][spec] ? `talentbg-${TALENT_BG[cls][spec]}.jpg` : null; return `
${bg ? `` : ''}
Arbre de talents — ${esc(specFr(spec) || spec || '?')} Arbre indisponible pour le moment.
`; } function talentTreeHTML(data) { if (!data || !data.trees) return '
Arbre de talents indisponible
'; const selByTalent = { class: {}, spec: {} }; const selected = data.selected || { class: [], spec: [] }; for (const k of ['class', 'spec']) { for (const t of (selected[k] || [])) { const arr = selByTalent[k][t.id] || (selByTalent[k][t.id] = []); arr.push(t.rank || 1); } } const CELL = 58, PAD = 8; const renderTree = (kind, title) => { const tree = data.trees[kind]; if (!tree || !tree.nodes || !tree.nodes.length) return ''; const w = (tree.width ?? 12) + 1; const h = (tree.height ?? 10) + 1; const nodes = tree.nodes.map((n) => { const ranks = selByTalent[kind][n.id] || selByTalent[kind][n.talent_id]; const on = ranks && ranks.length; const letter = esc((n.talent_name || '?')[0]); const badge = on ? `${ranks[0]}${n.max_rank > 1 ? '/' + n.max_rank : ''}` : ''; const tip = n.spell_id ? `data-wowhead="spell=${n.spell_id}&domain=fr" title="${esc(n.talent_name || n.spell_name || '')}"` : (n.talent_name ? `title="${esc(n.talent_name)}"` : ''); return ` ${n.icon ? `` : ''} ${letter} ${badge} `; }).join(''); return `
${title}${tree.name ? `${esc(tree.name)}` : ''}
${nodes}
`; }; const copyBtn = data.loadout_text ? `` : ''; return `
Talents — ${esc(specFr(data.spec) || data.spec)}${copyBtn}
${renderTree('class', 'Arbre de classe')} ${renderTree('spec', 'Arbre de spécialisation')}
`; } function renderStats(card) { const stats = []; stats.push(['ILVL', fmt(card.ilvl || null)]); stats.push(['Hauts faits', fmt(card.achievement_points || 0)]); stats.push(['Sets PvP', fmtSet(card.pvp_sets)]); stats.push(['Sets PvE', fmtSet(card.pve_sets)]); for (const [label, key] of [['Blitz', 'BLITZ'], ['Solo Shuffle', 'SOLO_SHUFFLE'], ['2v2', 'ARENA_2v2'], ['3v3', 'ARENA_3v3'], ['RBG', 'RATED_BG']]) { const rating = card.pvp && card.pvp[key] ? card.pvp[key].rating : null; const best = card.pvp_best && card.pvp_best[key] || null; const v = rating != null ? `${fmt(rating)}${best != null ? `/ ${fmt(best)}` : ''}` : '—'; stats.push([label, v]); } $('#modal-stats').innerHTML = stats .map(([l, v]) => `${l} ${v}`) .join(''); } $('#modal-close').addEventListener('click', closeModal); $('#class-modal-close').addEventListener('click', closeClassModal); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (!backdrop.hidden) closeModal(); if (!$('#class-modal-backdrop').hidden) closeClassModal(); } }); function initStarfield() { const field = $('#starfield'); if (!field) return; const count = Math.max(26, Math.min(70, Math.round((window.innerWidth * window.innerHeight) / 34000))); const color = '#f5a623'; let html = ''; for (let i = 0; i < count; i++) { const size = (Math.random() * 7 + 3.5).toFixed(2); const left = (Math.random() * 100).toFixed(2); const fall = (Math.random() * 16 + 12).toFixed(2); const sway = (Math.random() * 4 + 3.5).toFixed(2); const delay = (-Math.random() * 30).toFixed(2); const peak = (Math.random() * 0.25 + 0.12).toFixed(2); html += ``; } field.innerHTML = html; } initStarfield(); load(); })();