Skip to content

Instantly share code, notes, and snippets.

@phantom42
Created June 7, 2026 20:52
Show Gist options
  • Select an option

  • Save phantom42/09771f77f37792412d12ffa5034c7022 to your computer and use it in GitHub Desktop.

Select an option

Save phantom42/09771f77f37792412d12ffa5034c7022 to your computer and use it in GitHub Desktop.
Marvel CDB Card Annotation
// ==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