Created
June 7, 2026 20:52
-
-
Save phantom42/09771f77f37792412d12ffa5034c7022 to your computer and use it in GitHub Desktop.
Marvel CDB Card Annotation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name MarvelCDB – Card Type & Faction Labels | |
| // @namespace https://github.com/phantom42 | |
| // @version 1.0.0 | |
| // @description Annotates each card name on decklist/deck pages with its type and faction/aspect, read from the icon classes on each row. | |
| // @author phantom42 | |
| // @match https://marvelcdb.com/decklist/view/* | |
| // @match https://marvelcdb.com/deck/view/* | |
| // @match https://*.marvelcdb.com/decklist/view/* | |
| // @match https://*.marvelcdb.com/deck/view/* | |
| // @grant none | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ── Cosmetic config ────────────────────────────────────────────────────── | |
| const BADGE_STYLE = ` | |
| display: inline-block; | |
| margin-left: 6px; | |
| padding: 1px 5px; | |
| border-radius: 3px; | |
| font-size: 0.72em; | |
| font-weight: 600; | |
| letter-spacing: 0.04em; | |
| vertical-align: middle; | |
| line-height: 1.5; | |
| opacity: 0.9; | |
| `; | |
| // Aspect/faction → background color mapping (Marvel Champions palette) | |
| const FACTION_COLORS = { | |
| 'aggression': { bg: '#c0392b', fg: '#fff' }, | |
| 'justice': { bg: '#f1c40f', fg: '#000' }, | |
| 'leadership': { bg: '#2980b9', fg: '#fff' }, | |
| 'protection': { bg: '#27ae60', fg: '#fff' }, | |
| 'pool': { bg: '#e67e22', fg: '#fff' }, // Deadpool | |
| 'hero': { bg: '#2c3e50', fg: '#fff' }, | |
| 'campaign': { bg: '#7f8c8d', fg: '#fff' }, | |
| 'basic': { bg: '#95a5a6', fg: '#fff' }, | |
| }; | |
| // Type → background color mapping | |
| const TYPE_COLORS = { | |
| 'event': { bg: '#f39c12', fg: '#fff' }, | |
| 'resource': { bg: '#16a085', fg: '#fff' }, | |
| 'ally': { bg: '#2471a3', fg: '#fff' }, | |
| 'support': { bg: '#1a5276', fg: '#fff' }, | |
| 'upgrade': { bg: '#6c3483', fg: '#fff' }, | |
| 'attack': { bg: '#922b21', fg: '#fff' }, | |
| 'thwart': { bg: '#1f618d', fg: '#fff' }, | |
| 'defense': { bg: '#1e8449', fg: '#fff' }, | |
| 'action': { bg: '#784212', fg: '#fff' }, | |
| 'obligation': { bg: '#555', fg: '#fff' }, | |
| 'treachery': { bg: '#555', fg: '#fff' }, | |
| 'player_side_scheme': { bg: '#d35400', fg: '#fff' }, | |
| }; | |
| const DEFAULT_COLORS = { bg: '#888', fg: '#fff' }; | |
| // ── Helpers ─────────────────────────────────────────────────────────────── | |
| /** | |
| * Given a classList, pull out the "icon-X" values that aren't plain "icon". | |
| * Returns { type, faction } as human-readable strings. | |
| */ | |
| function extractIconInfo(classList) { | |
| const SKIP = new Set(['icon']); | |
| const iconClasses = [...classList].filter(c => c.startsWith('icon-') && !SKIP.has(c)); | |
| // MarvelCDB convention: icon-[type] comes before icon-[faction] | |
| // Known types: event, resource, ally, support, upgrade, attack, thwart, | |
| // defense, action, obligation, treachery | |
| // Known factions: aggression, justice, leadership, protection, hero, basic, pool, campaign | |
| const FACTIONS = new Set([ | |
| 'aggression','justice','leadership','protection', | |
| 'hero','basic','pool','campaign', | |
| ]); | |
| let type = null; | |
| let faction = null; | |
| for (const cls of iconClasses) { | |
| const val = cls.replace(/^icon-/, ''); | |
| if (FACTIONS.has(val)) { | |
| faction = val; | |
| } else { | |
| type = val; | |
| } | |
| } | |
| return { type, faction }; | |
| } | |
| /** Create a styled badge element. */ | |
| function makeBadge(label, colors) { | |
| const span = document.createElement('span'); | |
| span.textContent = label; | |
| span.setAttribute('style', | |
| BADGE_STYLE + | |
| `background:${colors.bg};color:${colors.fg};` | |
| ); | |
| return span; | |
| } | |
| /** Capitalize first letter of each word; replace underscores with spaces. */ | |
| function cap(str) { | |
| if (!str) return ''; | |
| return str.replace(/_/g, ' ') | |
| .replace(/\b\w/g, c => c.toUpperCase()); | |
| } | |
| // ── Main annotation logic ───────────────────────────────────────────────── | |
| /** | |
| * Process all card list items currently in the DOM. | |
| * Safe to call multiple times — already-annotated rows are skipped. | |
| */ | |
| function annotateCards() { | |
| // Each card row: <li> or <tr> containing a span.icon.icon-* | |
| // The card name link is the first <a> that is a sibling/descendant of that icon span. | |
| const iconSpans = document.querySelectorAll('span.icon[class*="icon-"]'); | |
| iconSpans.forEach(iconSpan => { | |
| // Skip if we've already annotated this row | |
| if (iconSpan.dataset.mcdAnnotated) return; | |
| iconSpan.dataset.mcdAnnotated = '1'; | |
| const { type, faction } = extractIconInfo(iconSpan.classList); | |
| if (!type && !faction) return; | |
| // Walk up to find the containing row element, then find the card name link. | |
| // MarvelCDB structure: the icon <span> and the card <a> are typically | |
| // siblings inside the same <td> or <li>. | |
| const container = iconSpan.closest('td, li, .card, [class*="card"]') | |
| ?? iconSpan.parentElement; | |
| if (!container) return; | |
| // The first anchor in the container that links to /card/ is the card name | |
| const cardLink = container.querySelector('a[href*="/card/"]'); | |
| if (!cardLink) return; | |
| // Already has badges? | |
| if (cardLink.nextElementSibling?.dataset?.mcdBadge) return; | |
| // Insert badges immediately after the card name link | |
| const fragment = document.createDocumentFragment(); | |
| if (faction) { | |
| const colors = FACTION_COLORS[faction] ?? DEFAULT_COLORS; | |
| const badge = makeBadge(cap(faction), colors); | |
| badge.dataset.mcdBadge = 'faction'; | |
| badge.title = `Aspect/faction: ${faction}`; | |
| fragment.appendChild(badge); | |
| } | |
| if (type) { | |
| const colors = TYPE_COLORS[type] ?? DEFAULT_COLORS; | |
| const badge = makeBadge(cap(type), colors); | |
| badge.dataset.mcdBadge = 'type'; | |
| badge.title = `Card type: ${type}`; | |
| fragment.appendChild(badge); | |
| } | |
| cardLink.after(fragment); | |
| }); | |
| } | |
| // ── Run ─────────────────────────────────────────────────────────────────── | |
| // Initial pass once the page is idle | |
| annotateCards(); | |
| // MarvelCDB has sort/filter controls that re-render the card list via JS. | |
| // Watch for DOM changes so we can re-annotate after re-sorts. | |
| const observer = new MutationObserver(mutations => { | |
| const relevant = mutations.some(m => | |
| [...m.addedNodes].some(n => n.nodeType === 1) | |
| ); | |
| if (relevant) annotateCards(); | |
| }); | |
| // Observe the deck section (or fall back to body) | |
| const deckSection = document.querySelector('.deck-list, #deck-panel, main, #content') | |
| ?? document.body; | |
| observer.observe(deckSection, { childList: true, subtree: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment