// === HALVSIES QUEST — ENGINE =========================================
// Palette, constants, bitmap font, helpers, overlays (dialog/title/etc.)

// ---------- Palette --------------------------------------------------
const C = {
  darkest:  "#0f380f",
  dark:     "#306230",
  light:    "#8bac0f",
  lightest: "#9bbc0f",
};

// ---------- Resolution ----------------------------------------------
const TILE  = 16;
const MAP_W = 12;
const MAP_H = 12;
const UI_H  = 24;
const W     = TILE * MAP_W;
const H     = TILE * MAP_H + UI_H;
const PLAY_TOP = UI_H;
const PLAY_H   = TILE * MAP_H;

// ---------- Timings -------------------------------------------------
const MOVE_MS         = 140;
const ATTACK_MS       = 220;
const INVULN_MS       = 1100;
const ENEMY_MOVE_MIN  = 520;
const ENEMY_MOVE_MAX  = 1000;
const NPC_MOVE_MIN    = 1100;
const NPC_MOVE_MAX    = 2200;
const INTRO_TEXT_MS   = 2400;
const INTRO_FADE_MS   = 700;
const TRANSITION_OUT  = 220;
const TRANSITION_IN   = 320;

const DIRS = {
  up:    { dx:  0, dy: -1 },
  down:  { dx:  0, dy:  1 },
  left:  { dx: -1, dy:  0 },
  right: { dx:  1, dy:  0 },
};

// =====================================================================
// BITMAP FONT (5x7)
// =====================================================================
const GLYPHS = {
  " ": [0,0,0,0,0,0,0],
  "A": [0b01110,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001],
  "B": [0b11110,0b10001,0b10001,0b11110,0b10001,0b10001,0b11110],
  "C": [0b01110,0b10001,0b10000,0b10000,0b10000,0b10001,0b01110],
  "D": [0b11110,0b10001,0b10001,0b10001,0b10001,0b10001,0b11110],
  "E": [0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b11111],
  "F": [0b11111,0b10000,0b10000,0b11110,0b10000,0b10000,0b10000],
  "G": [0b01110,0b10001,0b10000,0b10111,0b10001,0b10001,0b01111],
  "H": [0b10001,0b10001,0b10001,0b11111,0b10001,0b10001,0b10001],
  "I": [0b01110,0b00100,0b00100,0b00100,0b00100,0b00100,0b01110],
  "J": [0b00111,0b00010,0b00010,0b00010,0b00010,0b10010,0b01100],
  "K": [0b10001,0b10010,0b10100,0b11000,0b10100,0b10010,0b10001],
  "L": [0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111],
  "M": [0b10001,0b11011,0b10101,0b10101,0b10001,0b10001,0b10001],
  "N": [0b10001,0b11001,0b10101,0b10011,0b10001,0b10001,0b10001],
  "O": [0b01110,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110],
  "P": [0b11110,0b10001,0b10001,0b11110,0b10000,0b10000,0b10000],
  "Q": [0b01110,0b10001,0b10001,0b10001,0b10101,0b10010,0b01101],
  "R": [0b11110,0b10001,0b10001,0b11110,0b10100,0b10010,0b10001],
  "S": [0b01111,0b10000,0b10000,0b01110,0b00001,0b00001,0b11110],
  "T": [0b11111,0b00100,0b00100,0b00100,0b00100,0b00100,0b00100],
  "U": [0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b01110],
  "V": [0b10001,0b10001,0b10001,0b10001,0b10001,0b01010,0b00100],
  "W": [0b10001,0b10001,0b10001,0b10101,0b10101,0b10101,0b01010],
  "X": [0b10001,0b10001,0b01010,0b00100,0b01010,0b10001,0b10001],
  "Y": [0b10001,0b10001,0b10001,0b01010,0b00100,0b00100,0b00100],
  "Z": [0b11111,0b00001,0b00010,0b00100,0b01000,0b10000,0b11111],
  "0": [0b01110,0b10001,0b10011,0b10101,0b11001,0b10001,0b01110],
  "1": [0b00100,0b01100,0b00100,0b00100,0b00100,0b00100,0b01110],
  "2": [0b01110,0b10001,0b00001,0b00010,0b00100,0b01000,0b11111],
  "3": [0b11110,0b00001,0b00001,0b01110,0b00001,0b00001,0b11110],
  "4": [0b00010,0b00110,0b01010,0b10010,0b11111,0b00010,0b00010],
  "5": [0b11111,0b10000,0b11110,0b00001,0b00001,0b10001,0b01110],
  "6": [0b01110,0b10000,0b10000,0b11110,0b10001,0b10001,0b01110],
  "7": [0b11111,0b00001,0b00010,0b00100,0b01000,0b01000,0b01000],
  "8": [0b01110,0b10001,0b10001,0b01110,0b10001,0b10001,0b01110],
  "9": [0b01110,0b10001,0b10001,0b01111,0b00001,0b00001,0b01110],
  ".": [0,0,0,0,0,0b00110,0b00110],
  ",": [0,0,0,0,0b00110,0b00110,0b00100],
  "!": [0b00100,0b00100,0b00100,0b00100,0b00100,0,0b00100],
  "?": [0b01110,0b10001,0b00001,0b00010,0b00100,0,0b00100],
  "/": [0b00001,0b00010,0b00010,0b00100,0b01000,0b01000,0b10000],
  "-": [0,0,0,0b01110,0,0,0],
  "'": [0b00100,0b00100,0b00100,0,0,0,0],
  "$": [0b00100,0b01111,0b10100,0b01110,0b00101,0b11110,0b00100],
  ":": [0,0b00110,0b00110,0,0b00110,0b00110,0],
};

function glyph(ch) { return GLYPHS[ch.toUpperCase()] || GLYPHS["?"]; }

// =====================================================================
// TEXT RENDERING — Press Start 2P via ctx.fillText
// =====================================================================
// Glyph metrics for the Google Font 'Press Start 2P':
// designed at 8px (chars are exactly 8x8); at 16px chars are 16x16.
const PS2P_FONT_SM = "8px 'Press Start 2P', monospace";
const PS2P_FONT_LG = "16px 'Press Start 2P', monospace";
const PS2P_FONT_XS = "6px 'Press Start 2P', monospace";
const PS2P_CHAR_W_SM = 8;
const PS2P_CHAR_W_LG = 16;
const PS2P_CHAR_W_XS = 6;

function textWidth(str) { return str.length * PS2P_CHAR_W_SM; }
function bigTextWidth(str) { return str.length * PS2P_CHAR_W_LG; }
function textWidthSmall(str) { return str.length * PS2P_CHAR_W_XS; }

function drawText(ctx, str, x, y, color) {
  ctx.save();
  ctx.font = PS2P_FONT_SM;
  ctx.textBaseline = "top";
  ctx.fillStyle = color;
  ctx.fillText(str, Math.round(x), Math.round(y));
  ctx.restore();
}

function drawBigText(ctx, str, x, y, color) {
  ctx.save();
  ctx.font = PS2P_FONT_LG;
  ctx.textBaseline = "top";
  ctx.fillStyle = color;
  ctx.fillText(str, Math.round(x), Math.round(y));
  ctx.restore();
}

// Compact HUD variant — 6px PS2P so LIFE + hearts + wider screen labels coexist cleanly.
function drawTextSmall(ctx, str, x, y, color) {
  ctx.save();
  ctx.font = PS2P_FONT_XS;
  ctx.textBaseline = "top";
  ctx.fillStyle = color;
  ctx.fillText(str, Math.round(x), Math.round(y));
  ctx.restore();
}
function centerX(str, w)   { return Math.floor((w - textWidth(str)) / 2); }
function centerXBig(str, w){ return Math.floor((w - bigTextWidth(str)) / 2); }

// ---------- Word wrap + Pagination -----------------------------------
const DLG_LINE_CHARS = 22;   // chars per line in box at 8px PS2P
const DLG_LINES_PER_PAGE = 2;

function wrapText(str, maxChars) {
  const words = str.split(/\s+/);
  const lines = [];
  let cur = "";
  for (const w of words) {
    if (!cur) cur = w;
    else if ((cur + " " + w).length > maxChars) { lines.push(cur); cur = w; }
    else cur = cur + " " + w;
  }
  if (cur) lines.push(cur);
  return lines;
}

// Split a string into pages of up to N lines each.
function paginate(str, lineChars, linesPerPage) {
  const lines = wrapText(str, lineChars);
  const pages = [];
  for (let i = 0; i < lines.length; i += linesPerPage) {
    pages.push(lines.slice(i, i + linesPerPage));
  }
  if (!pages.length) pages.push([""]);
  return pages;
}

// =====================================================================
// HEARTS / TOP BAR
// =====================================================================
function drawHearts(ctx, hearts, max) {
  for (let i = 0; i < max; i++) drawHeart(ctx, 32 + i * 12, 7, i < hearts);
}

function drawHeart(ctx, x, y, filled) {
  ctx.fillStyle = C.darkest;
  ctx.fillRect(x+1, y, 3, 1);
  ctx.fillRect(x+5, y, 3, 1);
  ctx.fillRect(x,   y+1, 9, 4);
  ctx.fillRect(x+1, y+5, 7, 1);
  ctx.fillRect(x+2, y+6, 5, 1);
  ctx.fillRect(x+3, y+7, 3, 1);
  ctx.fillRect(x+4, y+8, 1, 1);
  const inner = filled ? C.light : C.dark;
  ctx.fillStyle = inner;
  ctx.fillRect(x+1, y+1, 3, 1);
  ctx.fillRect(x+5, y+1, 3, 1);
  ctx.fillRect(x+1, y+2, 7, 2);
  ctx.fillRect(x+2, y+4, 5, 1);
  ctx.fillRect(x+3, y+5, 3, 1);
  ctx.fillRect(x+4, y+6, 1, 1);
  if (filled) {
    ctx.fillStyle = C.lightest;
    ctx.fillRect(x+2, y+2, 1, 1);
    ctx.fillRect(x+3, y+1, 1, 1);
  }
}

function drawTopBar(ctx, screenLabel, hearts, opts) {
  ctx.fillStyle = C.dark;
  ctx.fillRect(0, 0, W, UI_H);
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, UI_H - 2, W, 2);
  // LIFE label — 6px PS2P so it doesn't bleed into the hearts
  drawTextSmall(ctx, "LIFE", 6, 10, C.lightest);
  // Hearts shifted right of LIFE (was x=32 — LIFE at 8px overlapped)
  drawHeartsAt(ctx, hearts, 3, 36);

  // Item indicators (right-aligned chunks)
  let rx = W - 6;
  if (opts && opts.hasSound) {
    rx -= 8;
    drawTapeIcon(ctx, rx, 7);
    rx -= 2;
  }
  if (opts && opts.hasGuitar) {
    rx -= 8;
    drawMiniGuitar(ctx, rx, 7);
    rx -= 2;
  }
  // Screen label — 6px PS2P so wide labels ("VILLAGE - WEST") still fit
  rx -= textWidthSmall(screenLabel);
  drawTextSmall(ctx, screenLabel, rx, 10, C.lightest);

  // Guitar Mode indicator (mini stave)
  if (opts && opts.guitarMode) {
    drawStaveBadge(ctx, W - 30, UI_H);
  }
}

function drawHeartsAt(ctx, hearts, max, startX) {
  for (let i = 0; i < max; i++) drawHeart(ctx, startX + i * 12, 7, i < hearts);
}

function drawTapeIcon(ctx, x, y) {
  // 8x8 cassette tape silhouette
  ctx.fillStyle = C.darkest;
  ctx.fillRect(x, y+1, 8, 6);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(x+1, y+2, 2, 2);
  ctx.fillRect(x+5, y+2, 2, 2);
  ctx.fillStyle = C.light;
  ctx.fillRect(x+1, y+5, 6, 1);
}

function drawMiniGuitar(ctx, x, y) {
  // 8x8 mini guitar
  ctx.fillStyle = C.lightest;
  ctx.fillRect(x, y+3, 4, 4);
  ctx.fillStyle = C.darkest;
  ctx.fillRect(x+1, y+4, 2, 2);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(x+4, y+4, 4, 1);
  ctx.fillRect(x+7, y+3, 1, 3);
}

function drawStaveBadge(ctx, x, y) {
  // Tiny floating stave next to the bar
  ctx.fillStyle = C.lightest;
  for (let r = 0; r < 5; r++) {
    ctx.fillRect(x, y + 2 + r * 2, 22, 1);
  }
  // treble flourish
  ctx.fillStyle = C.darkest;
  ctx.fillRect(x + 2, y + 1, 1, 11);
  ctx.fillRect(x + 1, y + 3, 3, 1);
  ctx.fillRect(x + 1, y + 6, 3, 1);
}

// =====================================================================
// DIALOG BOX (paginated)
// =====================================================================
function drawDialogBox(ctx, page, hasMore, t, choice) {
  const boxY = H - 50;
  const boxH = 46;

  // Border
  ctx.fillStyle = C.darkest;
  ctx.fillRect(2, boxY, W - 4, boxH);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(4, boxY + 2, W - 8, boxH - 4);
  ctx.fillStyle = C.darkest;
  ctx.fillRect(6, boxY + 4, W - 12, boxH - 8);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(7, boxY + 5, W - 14, boxH - 10);

  // Lines (max 2)
  const lines = page.slice(0, DLG_LINES_PER_PAGE);
  let ty = boxY + 10;
  for (const line of lines) {
    drawText(ctx, line, 11, ty, C.darkest);
    ty += 10;
  }

  // Choice (YES / NO) — rendered along the bottom-right
  if (choice) {
    const yes = choice.options[0];
    const no  = choice.options[1];
    const gap = 12;
    const yesW = textWidth(yes);
    const noW  = textWidth(no);
    const totalW = yesW + gap + noW + 16;
    let x = W - 12 - totalW + 12;
    const y = boxY + boxH - 12;
    // YES
    drawText(ctx, yes, x, y, C.darkest);
    if (choice.selected === 0) {
      ctx.fillStyle = C.darkest;
      ctx.fillRect(x - 6, y, 1, 5);
      ctx.fillRect(x - 5, y + 1, 1, 3);
      ctx.fillRect(x - 4, y + 2, 1, 1);
    }
    x += yesW + gap;
    drawText(ctx, no, x, y, C.darkest);
    if (choice.selected === 1) {
      ctx.fillStyle = C.darkest;
      ctx.fillRect(x - 6, y, 1, 5);
      ctx.fillRect(x - 5, y + 1, 1, 3);
      ctx.fillRect(x - 4, y + 2, 1, 1);
    }
    return;
  }

  // Blinking advance arrow
  const blink = Math.floor(t / 360) % 2 === 0;
  if (blink) {
    const ax = W - 14;
    const ay = boxY + boxH - 9;
    ctx.fillStyle = C.darkest;
    if (hasMore) {
      ctx.fillRect(ax,     ay,     5, 1);
      ctx.fillRect(ax + 1, ay + 1, 3, 1);
      ctx.fillRect(ax + 2, ay + 2, 1, 1);
    } else {
      ctx.fillRect(ax,     ay,     1, 5);
      ctx.fillRect(ax + 1, ay + 1, 1, 3);
      ctx.fillRect(ax + 2, ay + 2, 1, 1);
    }
  }
}

// =====================================================================
// SCREEN LABEL BANNER
// =====================================================================
function drawScreenLabelBanner(ctx, label, t) {
  if (t > 1400) return;
  const u = t < 700 ? 1 : 1 - (t - 700) / 700;
  ctx.globalAlpha = Math.max(0, u);
  const tw = textWidth(label);
  const padX = 6, padY = 4;
  const x = Math.floor((W - tw) / 2) - padX;
  const y = PLAY_TOP + 6;
  ctx.fillStyle = C.darkest;
  ctx.fillRect(x, y, tw + padX * 2, 7 + padY * 2);
  drawText(ctx, label, x + padX, y + padY, C.lightest);
  ctx.globalAlpha = 1;
}

// =====================================================================
// INTRO / TRANSITIONS
// =====================================================================
function drawIntro(ctx, t) {
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);
  const u = Math.min(1, t / 500);
  const fadeOut = t > INTRO_TEXT_MS - 600 ? Math.max(0, 1 - (t - (INTRO_TEXT_MS - 600)) / 600) : 1;
  ctx.globalAlpha = Math.min(u, fadeOut);
  const s = "WAKE UP...";
  drawText(ctx, s, centerX(s, W), 95, C.lightest);
  ctx.globalAlpha = 1;
}

function drawIntroFadeIn(ctx, progress) {
  ctx.globalAlpha = 1 - progress;
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);
  ctx.globalAlpha = 1;
}

function drawTransitionOverlay(ctx, progress) {
  ctx.globalAlpha = Math.max(0, Math.min(1, progress));
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);
  ctx.globalAlpha = 1;
}

// =====================================================================
// GAME OVER
// =====================================================================
function drawGameOver(ctx, choice, t) {
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);

  const l1 = "YOU ARE DOWN.";
  drawText(ctx, l1, centerX(l1, W), 68, C.lightest);

  const l2 = "GET UP AGAIN?";
  drawText(ctx, l2, centerX(l2, W), 100, C.light);

  const yes = "YES", no = "NO";
  const gap = 36;
  const yesW = textWidth(yes), noW = textWidth(no);
  const totalW = yesW + gap + noW;
  const startX = Math.floor((W - totalW) / 2);
  const yesX = startX;
  const noX  = startX + yesW + gap;
  drawText(ctx, yes, yesX, 128, choice === 0 ? C.lightest : C.dark);
  drawText(ctx, no,  noX,  128, choice === 1 ? C.lightest : C.dark);

  const blink = Math.floor(t / 320) % 2 === 0;
  if (blink) {
    const ax = (choice === 0 ? yesX : noX) - 8;
    ctx.fillStyle = C.lightest;
    ctx.fillRect(ax,     128, 1, 5);
    ctx.fillRect(ax + 1, 129, 1, 3);
    ctx.fillRect(ax + 2, 130, 1, 1);
  }

  const hint = "Z / ENTER TO CONFIRM";
  drawText(ctx, hint, centerX(hint, W), 178, C.dark);
}

// =====================================================================
// TITLE SCREEN
// =====================================================================
function drawTitleScreen(ctx, t, drawBard) {
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);
  drawBigText(ctx, "HALVSIES", centerXBig("HALVSIES", W), 40, C.lightest);
  drawBigText(ctx, "QUEST",    centerXBig("QUEST", W),    66, C.light);
  const bx = (W - 16) / 2;
  const by = 96;
  drawBard(ctx, bx, by, "down", Math.floor(t / 220), false);
  const blink = Math.floor(t / 480) % 2 === 0;
  if (blink) {
    const s = "PRESS Z TO START";
    drawText(ctx, s, centerX(s, W), 150, C.lightest);
  }
  const credit = "A BARD'S TALE";
  drawText(ctx, credit, centerX(credit, W), 175, C.dark);
}

// =====================================================================
// MONSTER FIELD VICTORY BANNER
// =====================================================================
function drawFieldClearedBanner(ctx) {
  const y0 = PLAY_TOP + 24;
  ctx.fillStyle = C.darkest;
  ctx.fillRect(8, y0, W - 16, 38);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(10, y0 + 2, W - 20, 34);
  ctx.fillStyle = C.darkest;
  ctx.fillRect(12, y0 + 4, W - 24, 30);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(13, y0 + 5, W - 26, 28);
  drawText(ctx, "YOU KILLED ALL",   centerX("YOU KILLED ALL",   W), y0 + 8,  C.darkest);
  drawText(ctx, "THE MONSTERS!",    centerX("THE MONSTERS!",    W), y0 + 17, C.darkest);
  drawText(ctx, "WHAT A JERK.",     centerX("WHAT A JERK.",     W), y0 + 26, C.darkest);
}

// =====================================================================
// GUITAR MODE — full music staff overlay
// =====================================================================
function drawGuitarHud(ctx, lastNote, lastNoteTimer) {
  // Big 5-line staff across the bottom
  const y0 = H - 36;
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, y0 - 4, W, 36);
  ctx.fillStyle = C.lightest;
  // staff lines
  for (let i = 0; i < 5; i++) ctx.fillRect(8, y0 + i * 4, W - 16, 1);
  // treble clef glyph
  ctx.fillRect(12, y0 - 2, 1, 18);
  ctx.fillRect(11, y0,     3, 1);
  ctx.fillRect(11, y0 + 8, 3, 1);
  ctx.fillRect(13, y0 + 4, 1, 4);

  // Note positions on staff (top to bottom of 5 lines): B5...F4
  // Map our 7 notes (C D E F G A B starting C4) to positions ~bottom-ish.
  // Quick mapping by staff index (line 5 = top):
  const noteY = {
    "C": y0 + 20,   // below staff
    "D": y0 + 18,
    "E": y0 + 16,   // bottom line
    "F": y0 + 14,
    "G": y0 + 12,
    "A": y0 + 10,
    "B": y0 + 8,
  };
  // Show all notes faintly (so player sees the palette), with labels
  const order = ["C","D","E","F","G","A","B"];
  ctx.fillStyle = C.light;
  for (let i = 0; i < order.length; i++) {
    const nx = 30 + i * 18;
    const ny = noteY[order[i]];
    // hollow note head
    ctx.fillRect(nx, ny, 4, 3);
    ctx.fillStyle = C.darkest;
    ctx.fillRect(nx + 1, ny + 1, 2, 1);
    ctx.fillStyle = C.light;
    // stem
    ctx.fillRect(nx + 3, ny - 6, 1, 7);
    drawText(ctx, order[i], nx, y0 + 24, C.lightest);
  }

  // Highlight the most-recent played note
  if (lastNote && lastNoteTimer > 0) {
    const i = order.indexOf(lastNote);
    if (i >= 0) {
      const nx = 30 + i * 18;
      const ny = noteY[lastNote];
      ctx.fillStyle = C.lightest;
      ctx.fillRect(nx - 1, ny - 1, 6, 5);
      ctx.fillStyle = C.darkest;
      ctx.fillRect(nx, ny, 4, 3);
      // tiny sparkle
      ctx.fillStyle = C.lightest;
      const spark = Math.floor(lastNoteTimer / 80) % 2;
      if (spark) ctx.fillRect(nx + 5, ny - 3, 1, 1);
    }
  }

  // label
  drawText(ctx, "STRUM:", 8, y0 - 14, C.lightest);
  drawText(ctx, "A=C  L=D  U=E  R=F  D=G", 8, y0 - 6, C.dark);
}

// =====================================================================
// FINALE — floating notes, crowd cheer overlay
// =====================================================================
function drawFloatingNote(ctx, n, t) {
  // n: { x, y, vy, t0, kind }
  const age = t - n.t0;
  const y = n.y - age * 0.024;
  const wobble = Math.sin((age + n.x * 7) / 220) * 3;
  const x = n.x + wobble;
  const alpha = Math.max(0, 1 - age / 3500);
  ctx.globalAlpha = alpha;
  ctx.fillStyle = C.darkest;
  // 8th note: filled head + stem (+ flag)
  if (n.kind === 0) {
    ctx.fillRect(x, y, 4, 3);
    ctx.fillRect(x + 1, y + 3, 3, 1);
    ctx.fillRect(x + 3, y - 6, 1, 7);
    ctx.fillRect(x + 4, y - 6, 2, 1);
    ctx.fillRect(x + 5, y - 5, 1, 2);
  } else if (n.kind === 1) {
    // quarter note
    ctx.fillRect(x, y, 4, 3);
    ctx.fillRect(x + 3, y - 8, 1, 8);
  } else {
    // beamed pair
    ctx.fillRect(x, y, 4, 3);
    ctx.fillRect(x + 6, y - 2, 4, 3);
    ctx.fillRect(x + 3, y - 8, 1, 8);
    ctx.fillRect(x + 9, y - 8, 1, 6);
    ctx.fillRect(x + 3, y - 8, 7, 1);
  }
  ctx.globalAlpha = 1;
}

// =====================================================================
// CREDITS
// =====================================================================
// Credits content per spec. `t` is ms since credits started.
// Returns `true` once the scroll is past the top (caller can show merch banner).
function drawCredits(ctx, t) {
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);
  const speed = 0.035;  // px / ms
  const startOffset = H + 24;
  const yStart = startOffset - t * speed;

  // Finalized credits. Wrapped to fit W=192 (max ~22 chars at 8px PS2P).
  const lines = [
    "",
    "A DUMB VIDEO GAME",
    "BY HALVSIES",
    "",
    "MADE BY HALVSIES",
    "",
    "--- TRACK LIST ---",
    "",
    "\"DOWN\" (8-BIT)",
    "BY HALVSIES",
    "",
    "\"SEVIER COUNTY",
    "KILLER\" (8-BIT)",
    "BY CHARLATAN",
    "PRODUCED BY",
    "TYLER BAKER CRAIG",
    "",
    "\"CVN'T DESTROY HER\"",
    "(8-BIT)",
    "BY CHARLATAN",
    "PRODUCED BY",
    "TYLER BAKER CRAIG",
    "",
    "(YES, CHARLATAN IS",
    "MY OTHER BAND.",
    "SHAMELESS DOUBLE-",
    "DIPPING SELF-",
    "PROMOTION IS A",
    "CORE MECHANIC OF",
    "THIS WEBSITE.)",
    "",
    "AN AGGRESSIVELY",
    "LOW-EFFORT DIGITAL",
    "MARKETING SCHEME",
    "BY HALVSIES",
    "",
    "",
    "FIN.",
  ];

  let lastY = yStart;
  for (let i = 0; i < lines.length; i++) {
    const y = yStart + i * 12;
    lastY = y;
    if (y < -8 || y > H) continue;
    const ln = lines[i];
    const accent = ln.includes("CREDITS") || ln === "FIN.";
    drawText(ctx, ln, centerX(ln, W), Math.floor(y), accent ? C.lightest : C.light);
  }
  // Done when last line scrolled above the top of the screen.
  return lastY < 24;
}

// Final unlock banner pop-up — shown over black after the credits finish.
function drawMerchUnlocked(ctx, t) {
  ctx.fillStyle = C.darkest;
  ctx.fillRect(0, 0, W, H);

  // Banner box centered
  const bw = W - 24;
  const bh = 96;
  const bx = 12;
  const by = (H - bh) / 2 - 4;

  // Outer frame
  ctx.fillStyle = C.lightest;
  ctx.fillRect(bx, by, bw, bh);
  ctx.fillStyle = C.darkest;
  ctx.fillRect(bx + 2, by + 2, bw - 4, bh - 4);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(bx + 4, by + 4, bw - 8, bh - 8);
  ctx.fillStyle = C.darkest;
  ctx.fillRect(bx + 6, by + 6, bw - 12, bh - 12);
  ctx.fillStyle = C.lightest;
  ctx.fillRect(bx + 7, by + 7, bw - 14, bh - 14);

  // Title
  drawText(ctx, "!!! UNLOCKED !!!", centerX("!!! UNLOCKED !!!", W), by + 14, C.darkest);

  // Big text
  drawBigText(ctx, "MERCH", centerXBig("MERCH", W), by + 26, C.darkest);
  drawBigText(ctx, "STORE", centerXBig("STORE", W), by + 46, C.darkest);

  // Small lower lines
  drawText(ctx, "YOU UNLOCKED THE",   centerX("YOU UNLOCKED THE",   W), by + 72, C.darkest);
  drawText(ctx, "MERCH STORE!",       centerX("MERCH STORE!",       W), by + 81, C.darkest);

  // Blinking continue prompt
  if (Math.floor(t / 360) % 2 === 0) {
    const s = "PRESS Z TO RETURN";
    drawText(ctx, s, centerX(s, W), H - 14, C.light);
  }

  // Confetti speckles (animated)
  for (let i = 0; i < 20; i++) {
    const seed = i * 73 + 1;
    const sx = (seed * 41 + Math.floor(t / 24)) % W;
    const sy = (seed * 53 + Math.floor(t / 18)) % (H - 40);
    if (sy < by - 2 || sy > by + bh + 2) {
      ctx.fillStyle = (i % 2) ? C.light : C.lightest;
      ctx.fillRect(sx, sy, 1, 1);
    }
  }
}

// =====================================================================
// EXPORTS
// =====================================================================
Object.assign(window, {
  // constants
  C, TILE, MAP_W, MAP_H, UI_H, W, H, PLAY_TOP, PLAY_H,
  MOVE_MS, ATTACK_MS, INVULN_MS, ENEMY_MOVE_MIN, ENEMY_MOVE_MAX,
  NPC_MOVE_MIN, NPC_MOVE_MAX,
  INTRO_TEXT_MS, INTRO_FADE_MS, TRANSITION_OUT, TRANSITION_IN,
  DIRS, DLG_LINE_CHARS, DLG_LINES_PER_PAGE,
  // font + helpers
  glyph, textWidth, drawText, drawBigText, drawTextSmall, textWidthSmall,
  centerX, centerXBig, wrapText, paginate,
  // UI / overlays
  drawHearts, drawHeart, drawTopBar,
  drawTapeIcon, drawMiniGuitar, drawStaveBadge,
  drawDialogBox, drawScreenLabelBanner,
  drawIntro, drawIntroFadeIn, drawTransitionOverlay,
  drawGameOver, drawTitleScreen, drawFieldClearedBanner,
  drawGuitarHud, drawFloatingNote, drawCredits, drawMerchUnlocked,
});
