PageSourceSearch

https://badmickey.netlify.app/shared/badge-art.mjs

js badmickey.netlify.app collected 2026-10-03 10:29:10 UTC 7,213 bytes, 189 lines download raw bytes

1/**
2 * Bad Crew badge artwork — THE single source of truth for badge imagery.
3 *
4 * This module is imported unchanged by both the browser (member dashboard, public
5 * roster) and the test suite, exactly like `badge-rules.mjs` and `bad-mickey.mjs`.
6 * It maps each badge key from the ladder onto the supplied artwork and says how
7 * that artwork must be framed. It contains NO qualification logic: whether a
8 * badge is earned is decided only by `computeBadgeState()` in `badge-rules.mjs`,
9 * and this module merely renders whatever that engine already decided.
10 *
11 * THERE IS NO GOOFY BADGE. The ladder ends at Bad Crew, and the Bad Crew badge is
12 * the full five-character group artwork — that group image IS what a member is
13 * working toward when they complete the Goofy stage of the ecosystem. The
14 * standalone Bad Goofy artwork is deliberately NOT in this map; it is kept in the
15 * repository for future Bad Goofy token/presale content only. See `UNUSED_ART`.
16 */
17import { BADGE_KEYS, getBadge } from './badge-rules.mjs';
18
19/**
20 * How a badge image is fitted into its frame.
21 *
22 * `cover` fills the frame and crops the edges — correct for the single-character
23 * portraits, which have one subject in the middle and nothing to lose at the
24 * sides. `contain` letterboxes instead of cropping, which is mandatory for the
25 * Bad Crew group image: all five characters have to stay visible, so it is never
26 * cropped down into a portrait shape.
27 */
28export const FIT = { COVER: 'cover', CONTAIN: 'contain' };
29
30export const ORIENTATION = { PORTRAIT: 'portrait', LANDSCAPE: 'landscape' };
31
32/**
33 * Badge key → artwork.
34 *
35 * `alt` is the accessible name and is required: badge state is never
36 * communicated by brightness or colour alone, and the image itself always
37 * announces which badge it depicts.
38 *
39 * `focus` is the CSS `object-position` used when `fit` is `cover`. The portraits
40 * are tall (2:3) with the character's face in the upper half, so the crop is
41 * biased upward — centring it would cut heads off on wide frames.
42 */
43export const BADGE_ART = {
44  bad_badge: {
45    src: '/assets/crew/badges/bad-badge.jpg',
46    alt: 'Bad Badge artwork',
47    orientation: ORIENTATION.PORTRAIT,
48    fit: FIT.COVER,
49    focus: '50% 22%',
50    /** Solo Bad Mickey. */
51    subject: 'Bad Mickey',
52  },
53  partners_in_crime: {
54    src: '/assets/crew/badges/partners-in-crime.jpg',
55    alt: 'Partners in Crime badge artwork',
56    orientation: ORIENTATION.PORTRAIT,
57    fit: FIT.COVER,
58    focus: '50% 28%',
59    /** Bad Mickey and Bad Minnie together — "he did not come back alone". */
60    subject: 'Bad Mickey and Bad Minnie',
61  },
62  getaway_crew: {
63    src: '/assets/crew/badges/getaway-crew.jpg',
64    alt: 'Getaway Crew badge artwork',
65    orientation: ORIENTATION.PORTRAIT,
66    fit: FIT.COVER,
67    focus: '50% 24%',
68    subject: 'Bad Donald',
69  },
70  double_cross: {
71    src: '/assets/crew/badges/double-cross.jpg',
72    alt: 'Double Cross badge artwork',
73    orientation: ORIENTATION.PORTRAIT,
74    fit: FIT.COVER,
75    focus: '50% 24%',
76    subject: 'Bad Daisy',
77  },
78  bad_crew: {
79    src: '/assets/crew/badges/bad-crew.jpg',
80    alt: 'Bad Crew badge artwork',
81    /**
82     * The only landscape badge, and the only one that must never be cropped:
83     * it is the full crew lineup, and forcing it into a portrait frame would
84     * delete members from the group.
85     */
86    orientation: ORIENTATION.LANDSCAPE,
87    fit: FIT.CONTAIN,
88    focus: '50% 50%',
89    subject: 'the full Bad Crew — Bad Minnie, Bad Donald, Bad Mickey, Bad Daisy and Bad Goofy',
90  },
91};
92
93/**
94 * Artwork that exists in the repository but is deliberately NOT a badge.
95 *
96 * The standalone Bad Goofy portrait is held for future Bad Goofy token and
97 * presale content. Completing the Goofy stage awards the Bad Crew badge (the
98 * group artwork above), so a Goofy badge does not exist and must never be added
99 * here without a product decision that also adds a sixth rung to the ladder.
100 */
101export const UNUSED_ART = ['/assets/bad-goofy.jpg'];
102
103/** Look up the artwork for a badge key. Returns null for anything unknown. */
104export function badgeArt(badgeKey) {
105  return BADGE_ART[badgeKey] ?? null;
106}
107
108/* ---------- Visual state ---------- */
109
110/**
111 * The three visually distinct badge states.
112 *
113 * These are DERIVED from the badge engine's own output. Nothing here can award
114 * or withhold a badge; `badgeArtState()` only reads the decision that
115 * `computeBadgeState()` already made.
116 */
117export const ART_STATE = {
118  /** Every requirement met, including verified Telegram. Full colour. */
119  EARNED: 'earned',
120  /** Wallet/token requirements met, Telegram not verified. NOT earned. */
121  AWAITING_TELEGRAM: 'awaiting-telegram',
122  /** Requirements not met. Dimmed and locked, but still visible. */
123  LOCKED: 'locked',
124};
125
126/**
127 * Primary status word for each state. Always rendered as text, so the state is
128 * never carried by colour or brightness alone.
129 */
130export const ART_STATE_LABEL = {
131  [ART_STATE.EARNED]: 'EARNED',
132  [ART_STATE.AWAITING_TELEGRAM]: 'WALLET REQUIREMENT MET',
133  [ART_STATE.LOCKED]: 'LOCKED',
134};
135
136/** Second line, present only where there is a specific action to name. */
137export const ART_STATE_HINT = {
138  [ART_STATE.EARNED]: 'Badge awarded',
139  [ART_STATE.AWAITING_TELEGRAM]: 'VERIFY TELEGRAM TO UNLOCK',
140  [ART_STATE.LOCKED]: 'Requirements not met',
141};
142
143/**
144 * Map one tier of the engine's output onto an artwork state.
145 *
146 * `unlocked` and `awaitingTelegram` are computed in `computeBadgeState()`:
147 * `unlocked` already includes the verified-Telegram award gate, and
148 * `awaitingTelegram` is precisely "wallet qualified, badge withheld". Reading
149 * them here — rather than re-deriving anything from amounts — is what keeps the
150 * visuals honest.
151 *
152 * @param {{unlocked?: boolean, awaitingTelegram?: boolean}} tierState
153 */
154export function badgeArtState(tierState) {
155  if (tierState?.unlocked === true) return ART_STATE.EARNED;
156  if (tierState?.awaitingTelegram === true) return ART_STATE.AWAITING_TELEGRAM;
157  return ART_STATE.LOCKED;
158}
159
160/** True only for the fully awarded state. */
161export function isEarnedState(state) {
162  return state === ART_STATE.EARNED;
163}
164
165/**
166 * Accessible name for a badge image including its status, e.g.
167 * "Bad Crew badge artwork — LOCKED". Used where the status text is not already
168 * adjacent to the image in the reading order.
169 */
170export function badgeArtLabel(badgeKey, state) {
171  const art = badgeArt(badgeKey);
172  const badge = getBadge(badgeKey);
173  const name = art ? art.alt : `${badge ? badge.name : 'Badge'} artwork`;
174  const status = ART_STATE_LABEL[state];
175  return status ? `${name} — ${status}` : name;
176}
177
178/**
179 * Every badge on the ladder has artwork, and there is artwork for nothing else.
180 * Exported so both the test suite and a boot-time check can assert it.
181 */
182export function badgeArtCoverageError() {
183  const artKeys = Object.keys(BADGE_ART);
184  const missing = BADGE_KEYS.filter((key) => !artKeys.includes(key));
185  const extra = artKeys.filter((key) =>
185 !BADGE_KEYS.includes(key));
186  if (missing.length) return `badges without artwork: ${missing.join(', ')}`;
187  if (extra.length) return `artwork for badges that do not exist: ${extra.join(', ')}`;
188  return null;
189}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.