From 2af7041a3967f78d6cfda516f76209d87f523f70 Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Mon, 13 Jul 2026 21:21:37 +0200 Subject: [PATCH 1/3] Add README and CI stub, remove duplicate root styles.css Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 10 + README.md | 36 + styles.css | 1556 -------------------------------------- 3 files changed, 46 insertions(+), 1556 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 README.md delete mode 100644 styles.css diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..781167c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,10 @@ +name: ci +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ci: + uses: got-feedback/.github/.github/workflows/reusable-ci.yml@main diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c91f67 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Bongo Cat's Rhythm Trainer + +A feedBack minigame plugin for practicing rhythms on your real instrument — guitar, bass, piano, or drums. Bongo Cat demos a rhythm pattern in time with the metronome, then you play it back; your hits are picked up through feedBack's note detection and judged against the beat. + +## How it works + +- Pick an instrument, mode, and tempo from the minigame modifiers. +- Bongo Cat plays the pattern first (with paw animations synced to the beat), and a notation strip shows what to play. +- Play the pattern back. Each hit is scored discretely (timing judgement per note), with a HUD tracking your accuracy and streak. +- **Learning mode** eases you in and repeats patterns; **Challenge mode** keeps generating new ones and pushes the difficulty. +- Run history is kept per session so you can see your progress over time. + +## Modifiers + +| Modifier | Values | Default | +|---|---|---| +| Instrument | guitar, piano, drums | guitar | +| Mode | learning, challenge | learning | +| Tempo (BPM) | 60–160 in steps | 80 | +| Calibration | auto, on, off | auto | + +## Structure + +- `game.js` — plugin entry point +- `src/` — game logic: beat clock, pattern generator, input gate/judge, scoring, run controller, FSM, HUD, notation strip, cat animations +- `src/notedetect-bridge.js` — bridge to feedBack's note-detection plugin +- `routes.py` — server-side routes +- `assets/` — sprites, fonts, styles +- `docs/implementation-artifacts/` — design notes per feature + +## Development + +```bash +npm install +npm test # vitest +``` diff --git a/styles.css b/styles.css deleted file mode 100644 index 70c2cec..0000000 --- a/styles.css +++ /dev/null @@ -1,1556 +0,0 @@ -/* ============================================================ - Bongo Cat's Rhythm Trainer — scoped under .bc-game-root - ============================================================ */ - -/* Import BPM animation keyframes (task 5.2) */ -@import url("src/bpm-motion.css"); - -/* --- Root element reset + Design System tokens (E5S1) --- */ -.bc-game-root { - /* ---- Design System ---- - Sticker Arcade visual language tokens. - Reference these via var() instead of hardcoded values. */ - - /* Color tokens — Light mode (default) */ - --bc-bg: #E8F4FC; - --bc-surface: #FDFEFF; - --bc-border: #E2E8F0; - --bc-ink: #1E3A52; - --bc-primary: #0EA5E9; - --bc-secondary: #D194B8; - --bc-perfect: #3FA97E; - --bc-good: #7C89E0; - --bc-late: #D8A03E; - --bc-miss: #CC6E82; - --bc-hearts: #E08CA4; - --bc-streak: #38BDF8; - --bc-streak-grad: linear-gradient(90deg, #38BDF8, #B9A6E0); - - /* Sticker rim tokens */ - --bc-sticker-rim-color: #F8FAFC; - --bc-sticker-rim-width: 4px; - - /* Shadow tokens */ - --bc-shadow-offset-x: 5px; - --bc-shadow-offset-y: 6px; - --bc-shadow-color: rgba(0, 0, 0, 0.45); - --bc-shadow-btn-offset-x: 3px; - --bc-shadow-btn-offset-y: 4px; - --bc-shadow-btn-color: rgba(0, 0, 0, 0.3); - - /* Corner radii */ - --bc-panel-radius: 18px; - --bc-chip-radius: 999px; - --bc-notation-slot-radius: 12px; - --bc-setup-control-radius: 12px; - - /* Spacing */ - --bc-panel-gap: 12px; - --bc-panel-padding: 16px; - - /* Sticker rotation (small offset for hand-placed feel) */ - --bc-panel-rotation: 0.5deg; - - /* Typography */ - --bc-font-family: "Averia Libre", ui-rounded, system-ui, sans-serif; - --bc-font-mono: ui-monospace, "SF Mono", "Fira Code", "Consolas", monospace; - - /* Base layout */ - display: block; - box-sizing: border-box; - font-family: var(--bc-font-family); - line-height: 1.4; - color: var(--bc-ink); - background: var(--bc-bg); - - /* Responsive: 16:9 gameplay area awareness */ - aspect-ratio: 16 / 9; - max-width: 100%; - max-height: 100vh; - overflow: hidden; -} - -/* --- Dark Mode --- */ -@media (prefers-color-scheme: dark) { - .bc-game-root { - --bc-bg: #0F172A; - --bc-surface: #1E293B; - --bc-border: #334155; - --bc-ink: #F8FAFC; - --bc-primary: #38BDF8; - --bc-secondary: #E0A9CB; - --bc-perfect: #6FC9A2; - --bc-good: #9BA8F0; - --bc-late: #E7B95F; - --bc-miss: #E092A4; - --bc-hearts: #EBA6BC; - --bc-streak: #7DD3FC; - --bc-streak-grad: linear-gradient(90deg, #7DD3FC, #C3B1EC); - --bc-sticker-rim-color: #1E293B; - --bc-shadow-color: rgba(0, 0, 0, 0.6); - --bc-shadow-btn-color: rgba(0, 0, 0, 0.45); - } -} - -.bc-game-root *, -.bc-game-root *::before, -.bc-game-root *::after { - box-sizing: inherit; -} - -/* ============================================================ - Typography Utility Classes (E5S1) - ============================================================ */ - -.bc-display { - font: 700 24px/1.2 var(--bc-font-family); -} - -.bc-display-sm { - font: 700 20px/1.2 var(--bc-font-family); -} - -.bc-body { - font: 400 13px/1.4 var(--bc-font-family); -} - -.bc-label { - font: 700 11px/1 var(--bc-font-family); - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.bc-grade { - font: 800 10px/1.4 var(--bc-font-family); -} - -.bc-mono { - font: 400 10px/1.4 var(--bc-font-mono); -} - -/* ============================================================ - Sticker Panel Base Class (E5S1) - ============================================================ */ - -.bc-sticker-panel { - background: var(--bc-surface); - border: var(--bc-sticker-rim-width) solid var(--bc-sticker-rim-color); - border-radius: var(--bc-panel-radius); - box-shadow: var(--bc-shadow-offset-x) var(--bc-shadow-offset-y) 0 var(--bc-shadow-color); - transform: rotate(var(--bc-panel-rotation)); - padding: var(--bc-panel-padding); - transition: transform 0.15s ease, box-shadow 0.15s ease; -} - -/* ============================================================ - Button Classes (E5S1) - ============================================================ */ - -.bc-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 12px 24px; - border: var(--bc-sticker-rim-width) solid var(--bc-sticker-rim-color); - border-radius: var(--bc-setup-control-radius); - font: 700 15px/1.2 var(--bc-font-family); - cursor: pointer; - transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.15s ease; - text-align: center; - line-height: 1.2; - user-select: none; -} - -.bc-btn:focus-visible { - outline: 3px solid var(--bc-primary); - outline-offset: 2px; -} - -.bc-btn:active { - transform: translateY(1px); -} - -.bc-btn--primary { - background: var(--bc-primary); - color: var(--bc-ink); - box-shadow: var(--bc-shadow-btn-offset-x) var(--bc-shadow-btn-offset-y) 0 var(--bc-shadow-btn-color); -} - -.bc-btn--primary:hover { - transform: translateY(-1px); - box-shadow: calc(var(--bc-shadow-btn-offset-x) + 1px) calc(var(--bc-shadow-btn-offset-y) + 1px) 0 var(--bc-shadow-btn-color); -} - -.bc-btn--primary:active { - box-shadow: 1px 2px 0 var(--bc-shadow-btn-color); -} - -.bc-btn--secondary { - background: var(--bc-surface); - color: var(--bc-ink); - box-shadow: var(--bc-shadow-btn-offset-x) var(--bc-shadow-btn-offset-y) 0 rgba(0, 0, 0, 0.2); -} - -.bc-btn--secondary:hover { - transform: translateY(-1px); -} - -.bc-btn--secondary:active { - box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.2); -} - -.bc-btn--ghost { - background: transparent; - color: var(--bc-ink); - border-color: transparent; -} - -.bc-btn--disabled, -.bc-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; - box-shadow: none !important; -} - -/* ============================================================ - Segmented Control Classes (E5S1) - ============================================================ */ - -.bc-segmented { - display: flex; - gap: 0; - border: 2px solid var(--bc-border); - border-radius: var(--bc-setup-control-radius); - overflow: hidden; -} - -.bc-segmented-btn { - flex: 1; - padding: 8px 16px; - border: none; - background: var(--bc-surface); - color: var(--bc-ink); - font: 500 13px/1 var(--bc-font-family); - cursor: pointer; - transition: background 0.15s ease, color 0.15s ease; - outline: none; -} - -.bc-segmented-btn + .bc-segmented-btn { - border-left: 1px solid var(--bc-border); -} - -.bc-segmented-btn--active { - background: var(--bc-primary); - color: var(--bc-ink); - font-weight: 700; -} - -.bc-segmented-btn:focus-visible { - outline: 3px solid var(--bc-primary); - outline-offset: -3px; - z-index: 1; -} - -.bc-segmented-btn:hover:not(.bc-segmented-btn--active):not(:disabled) { - background: var(--bc-bg); -} - -.bc-segmented-btn--disabled, -.bc-segmented-btn:disabled { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; - background: var(--bc-border); - color: var(--bc-ink); -} - -/* ============================================================ - Grade Badge Styles (E5S1) — colorblind-safe: icon + text label - ============================================================ */ - -.bc-grade-badge { - display: inline-flex; - align-items: center; - gap: 4px; - border-radius: var(--bc-chip-radius); - padding: 2px 8px; - font: 800 10px/1.4 var(--bc-font-family); - color: var(--bc-bg); -} - -.bc-grade-perfect { - background: var(--bc-perfect); -} - -.bc-grade-good { - background: var(--bc-good); -} - -.bc-grade-late { - background: var(--bc-late); -} - -.bc-grade-miss { - background: var(--bc-miss); -} - -/* ============================================================ - Notation Beam (E5S1) - ============================================================ */ - -.bc-notation-beam { - height: 5px; - background: var(--bc-ink); - opacity: 0.85; - border-radius: 3px; -} - -/* ============================================================ - Active Slot (E5S1) - ============================================================ */ - -.bc-active-slot { - outline: 4px solid var(--bc-primary); - outline-offset: 5px; - border-radius: var(--bc-notation-slot-radius); - transform: scale(1.04); - transition: transform 100ms ease-out; -} - -/* ============================================================ - Input Readiness Gate (E5S1) - ============================================================ */ - -.bc-input-gate-idle { - border: 2px dashed var(--bc-late); -} - -.bc-input-gate-ready { - border-color: var(--bc-primary); -} - -/* ============================================================ - Single-column Run Setup (E5S1) - ============================================================ */ - -.bc-setup-panel { - max-width: 520px; - margin: 0 auto; -} - -/* --- Hub tile (sticker panel card) --- */ -.bc-game-root__hub-tile { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - padding: 24px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); - transform: rotate(0deg); - transition: transform 0.15s ease, box-shadow 0.15s ease; - cursor: pointer; -} - -.bc-game-root__hub-tile:hover { - transform: rotate(-1deg); - box-shadow: 6px 7px 0 rgba(0, 0, 0, 0.5); -} - -.bc-game-root__hub-tile:active { - transform: rotate(1deg); - box-shadow: 3px 4px 0 rgba(0, 0, 0, 0.4); -} - -/* --- Thumbnail --- */ -.bc-game-root__hub-thumbnail { - width: 120px; - height: 120px; - object-fit: cover; - border-radius: 12px; - border: 2px solid #e2e8f0; - background: #f1f5f9; -} - -/* --- Title --- */ -.bc-game-root__hub-title { - margin: 0; - font-size: 18px; - font-weight: 700; - color: #0f172a; - text-align: center; - letter-spacing: -0.01em; -} - -/* --- Summary line --- */ -.bc-game-root__hub-summary { - margin: 0; - font-size: 13px; - color: #64748b; - text-align: center; - line-height: 1.5; -} - -/* --- Surface container --- */ -.bc-game-root__surface { - position: relative; - width: 100%; - height: 100%; -} - -/* --- Loading state --- */ -.bc-game-root__loading { - padding: 32px; - text-align: center; - color: #94a3b8; - font-size: 14px; -} - -/* ============================================================ - Run Setup — single-column sticker panel - ============================================================ */ - -.bc-game-root__run-setup { - display: flex; - flex-direction: column; - gap: 16px; - max-width: 520px; - margin: 0 auto; - padding: 24px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); - transform: rotate(0deg); -} - -/* --- Label role (11px/700/0.1em) --- */ -.bc-game-root__label { - display: block; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - color: #1e3a52; - margin-bottom: 4px; -} - -/* ============================================================ - Segmented Control - ============================================================ */ - -.bc-game-root__segmented { - display: flex; - gap: 0; - border: 2px solid #e2e8f0; - border-radius: 12px; - overflow: hidden; -} - -.bc-game-root__segmented-btn { - flex: 1; - padding: 8px 16px; - border: none; - background: #fdfeff; - color: #1e3a52; - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: background 0.15s ease, color 0.15s ease; - outline: none; -} - -.bc-game-root__segmented-btn + .bc-game-root__segmented-btn { - border-left: 1px solid #e2e8f0; -} - -.bc-game-root__segmented-btn--active { - background: #0ea5e9; - color: #1e3a52; - font-weight: 700; -} - -.bc-game-root__segmented-btn:focus-visible { - outline: 3px solid #0ea5e9; - outline-offset: -3px; - z-index: 1; -} - -.bc-game-root__segmented-btn:hover:not(.bc-game-root__segmented-btn--active):not(:disabled) { - background: #f1f5f9; -} - -.bc-game-root__segmented-btn:disabled, -.bc-game-root__segmented-btn--disabled { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; - background: #e2e8f0; - color: #94a3b8; -} - -/* --- Mode description note --- */ -.bc-game-root__mode-note { - margin: 0; - font-size: 13px; - font-weight: 400; - color: #64748b; - line-height: 1.5; - padding: 0 4px; -} - -/* ============================================================ - BPM Slider - ============================================================ */ - -.bc-game-root__bpm-row { - display: flex; - align-items: center; - gap: 12px; -} - -.bc-game-root__bpm-slider { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: #e2e8f0; - border-radius: 3px; - outline: none; - cursor: pointer; -} - -.bc-game-root__bpm-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: #0ea5e9; - border: 2px solid #f8fafc; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - cursor: pointer; -} - -.bc-game-root__bpm-slider::-moz-range-thumb { - width: 20px; - height: 20px; - border-radius: 50%; - background: #0ea5e9; - border: 2px solid #f8fafc; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - cursor: pointer; -} - -.bc-game-root__bpm-slider:focus-visible { - outline: 3px solid #0ea5e9; - outline-offset: 2px; -} - -.bc-game-root__bpm-slider:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.bc-game-root__bpm-value { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 10px; - font-weight: 600; - color: #1e3a52; - min-width: 60px; - text-align: right; - white-space: nowrap; -} - -/* --- Start button --- */ -.bc-game-root__start-btn { - display: block; - width: 100%; - padding: 12px 24px; - border: 4px solid #f8fafc; - border-radius: 12px; - background: #0ea5e9; - color: #1e3a52; - font-size: 15px; - font-weight: 700; - cursor: pointer; - box-shadow: 3px 4px 0 rgba(0, 0, 0, 0.3); - transition: transform 0.1s ease, box-shadow 0.1s ease; -} - -.bc-game-root__start-btn:hover { - transform: translateY(-1px); - box-shadow: 4px 5px 0 rgba(0, 0, 0, 0.35); -} - -.bc-game-root__start-btn:active { - transform: translateY(1px); - box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.3); -} - -.bc-game-root__start-btn:focus-visible { - outline: 3px solid #0ea5e9; - outline-offset: 2px; -} - -/* --- aria-live region (visually hidden) --- */ -.bc-game-root__aria-live { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -/* ============================================================ - Input Readiness Gate - ============================================================ */ - -.bc-game-root__input-gate { - padding: 12px 16px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); -} - -.bc-game-root__gate-container { - margin-top: 8px; -} - -.bc-game-root__gate-row { - display: flex; - align-items: center; - gap: 10px; -} - -.bc-game-root__gate-dot { - flex-shrink: 0; - width: 14px; - height: 14px; - border-radius: 50%; - border: 2px dashed #d8a03e; - background: #334155; - transition: background 0.3s ease, border-color 0.3s ease, border-style 0.3s ease; -} - -.bc-game-root__gate-dot--idle { - border-color: #d8a03e; - border-style: dashed; - background: #334155; -} - -.bc-game-root__gate-dot--live { - border-color: #0ea5e9; - border-style: solid; - background: #0ea5e9; - animation: bc-pulse 0.8s ease-in-out infinite alternate; -} - -.bc-game-root__gate-dot--error { - border-color: #ef4444; - border-style: solid; - background: #ef4444; -} - -@keyframes bc-pulse { - from { - box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.4); - } - to { - box-shadow: 0 0 0 6px rgba(14, 165, 233, 0); - } -} - -.bc-game-root__gate-message { - margin: 0; - font-size: 13px; - font-weight: 400; - color: #64748b; - line-height: 1.5; - flex: 1; -} - -/* ============================================================ - Calibration Prompt - ============================================================ */ - -.bc-game-root__calibration { - margin-top: 8px; -} - -.bc-game-root__cal-prompt { - padding: 16px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); -} - -.bc-game-root__cal-text { - margin: 0 0 12px; - font-size: 13px; - font-weight: 400; - color: #64748b; - line-height: 1.5; -} - -.bc-game-root__cal-btn-row { - display: flex; - gap: 8px; -} - -.bc-game-root__cal-btn { - flex: 1; - padding: 8px 16px; - border: 2px solid #f8fafc; - border-radius: 12px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: background 0.15s ease, transform 0.1s ease; -} - -.bc-game-root__cal-btn--primary { - background: #0ea5e9; - color: #1e3a52; - box-shadow: 2px 3px 0 rgba(0, 0, 0, 0.3); -} - -.bc-game-root__cal-btn--primary:active { - transform: translateY(1px); - box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.3); -} - -.bc-game-root__cal-btn--secondary { - background: #fdfeff; - color: #1e3a52; - box-shadow: 2px 3px 0 rgba(0, 0, 0, 0.2); -} - -.bc-game-root__cal-btn--secondary:active { - transform: translateY(1px); - box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.2); -} - -/* --- Calibration flow (mini-flow) --- */ -.bc-game-root__cal-flow { - padding: 16px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); - text-align: center; -} - -.bc-game-root__cal-flow-status { - margin: 0 0 8px; - font-size: 13px; - color: #64748b; -} - -.bc-game-root__cal-paw { - display: inline-block; - font-size: 32px; - opacity: 0.3; - transition: opacity 0.1s ease, transform 0.1s ease; -} - -.bc-game-root__cal-paw--active { - opacity: 1; - transform: scale(1.2); -} - -/* --- Calibration done --- */ -.bc-game-root__cal-done { - margin: 0; - padding: 12px 16px; - font-size: 13px; - font-weight: 500; - color: #0ea5e9; - text-align: center; -} - -/* --- Calibration warning --- */ -.bc-game-root__cal-warning { - padding: 12px 16px; - font-size: 13px; - color: #d8a03e; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); - background: #ffffff; - text-align: center; -} - -/* --- Disabled Start button --- */ -.bc-game-root__start-btn--disabled { - background: #334155 !important; - color: #94a3b8 !important; - box-shadow: none !important; - cursor: default !important; - pointer-events: none; - opacity: 0.6; -} - -/* ============================================================ - Cat Demo — bongo cat visual metronome (AD-7) - ============================================================ */ - -.bc-game-root__cat-demo { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - padding: 20px; - user-select: none; -} - -/* --- State visibility --- */ -.bc-game-root__cat-demo--idle .bc-game-root__cat-demo-label, -.bc-game-root__cat-demo--idle .bc-game-root__cat-demo-beat-count { - opacity: 0; -} - -.bc-game-root__cat-demo--done .bc-game-root__cat-demo-paw--hit { - animation: none; -} - -/* --- Label --- */ -.bc-game-root__cat-demo-label { - font-size: 14px; - font-weight: 700; - color: #1e3a52; - letter-spacing: 0.05em; - text-align: center; - min-height: 1.4em; - transition: opacity 0.2s ease; -} - -/* --- Stage --- */ -.bc-game-root__cat-demo-stage { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -/* --- Cat body --- */ -.bc-game-root__cat-demo-body { - display: flex; - flex-direction: column; - align-items: center; -} - -/* --- Head --- */ -.bc-game-root__cat-demo-head { - position: relative; - width: 100px; - height: 82px; - background: #fbbf24; - border-radius: 50% 50% 45% 45%; - border: 2px solid #d97706; -} - -/* --- Ears --- */ -.bc-game-root__cat-demo-ear { - position: absolute; - width: 24px; - height: 22px; - background: #fbbf24; - border: 2px solid #d97706; - top: -12px; - z-index: -1; -} - -.bc-game-root__cat-demo-ear--left { - left: 10px; - border-radius: 50% 0 0 0; - transform: rotate(-15deg); -} - -.bc-game-root__cat-demo-ear--right { - right: 10px; - border-radius: 0 50% 0 0; - transform: rotate(15deg); -} - -/* --- Face features --- */ -.bc-game-root__cat-demo-face { - position: absolute; - top: 22px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 10px; - align-items: flex-start; - width: 60px; -} - -.bc-game-root__cat-demo-eye { - width: 10px; - height: 12px; - background: #1e293b; - border-radius: 50%; -} - -.bc-game-root__cat-demo-eyes-open .bc-game-root__cat-demo-eye { - height: 12px; -} - -.bc-game-root__cat-demo-nose { - width: 8px; - height: 6px; - background: #f472b6; - border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; - margin-top: 4px; - flex-basis: 100%; - text-align: center; -} - -.bc-game-root__cat-demo-mouth { - width: 16px; - height: 8px; - border-bottom: 2px solid #92400e; - border-radius: 0 0 50% 50%; - flex-basis: 100%; - margin-top: 2px; -} - -/* --- Paws --- */ -.bc-game-root__cat-demo-paws { - display: flex; - gap: 16px; - margin-top: -6px; -} - -.bc-game-root__cat-demo-paw { - width: 28px; - height: 24px; - background: #fbbf24; - border: 2px solid #d97706; - border-radius: 40% 40% 50% 50%; - transition: transform 0.06s ease, background 0.06s ease; - position: relative; -} - -/* Paw pads (claw marks) */ -.bc-game-root__cat-demo-paw::before { - content: ""; - position: absolute; - top: 6px; - left: 50%; - transform: translateX(-50%); - width: 6px; - height: 5px; - background: #fef3c7; - border-radius: 50%; - opacity: 0.5; -} - -.bc-game-root__cat-demo-paw--hit { - transform: translateY(10px) scaleY(0.85); - background: #f59e0b; -} - -/* --- Drums --- */ -.bc-game-root__cat-demo-drums { - display: flex; - gap: 16px; - margin-top: 2px; -} - -.bc-game-root__cat-demo-drum { - width: 36px; - height: 20px; - background: #94a3b8; - border: 2px solid #64748b; - border-radius: 4px 4px 8px 8px; - position: relative; -} - -.bc-game-root__cat-demo-drum::after { - content: ""; - position: absolute; - top: -3px; - left: -3px; - right: -3px; - height: 5px; - background: #cbd5e1; - border: 2px solid #64748b; - border-radius: 3px; -} - -.bc-game-root__cat-demo-drum--left { - transform: rotate(-2deg); -} - -.bc-game-root__cat-demo-drum--right { - transform: rotate(2deg); -} - -/* --- Beat count --- */ -.bc-game-root__cat-demo-beat-count { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 12px; - font-weight: 600; - color: #64748b; - min-width: 24px; - text-align: center; - transition: opacity 0.2s ease; -} - -/* ============================================================ - Cat Expressions — face reacting to Judge grades (task 3.2) - ============================================================ */ - -.bc-game-root__cat-expressions { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 12px 16px; - user-select: none; - transition: opacity 0.2s ease; -} - -/* --- Face display (emoji/text expression) --- */ -.bc-game-root__cat-expressions-face { - font-size: 28px; - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-weight: 700; - line-height: 1.4; - letter-spacing: 0.05em; - text-align: center; - transition: transform 0.15s ease, color 0.15s ease; - min-height: 1.4em; - min-width: 80px; -} - -/* --- State-specific face styles --- */ -.bc-game-root__cat-expressions--idle .bc-game-root__cat-expressions-face { - color: #64748b; - transform: scale(1); -} - -.bc-game-root__cat-expressions--focusing .bc-game-root__cat-expressions-face { - color: #1e3a52; - transform: scale(1.05); -} - -.bc-game-root__cat-expressions--happy .bc-game-root__cat-expressions-face { - color: #16a34a; - transform: scale(1.15); -} - -.bc-game-root__cat-expressions--good .bc-game-root__cat-expressions-face { - color: #0ea5e9; - transform: scale(1.08); -} - -.bc-game-root__cat-expressions--sad .bc-game-root__cat-expressions-face { - color: #ef4444; - transform: scale(0.95); -} - -/* --- Aria-live label (visually hidden) --- */ -.bc-game-root__cat-expressions-label { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -/* ============================================================ - Notation Strip — horizontal pattern visualisation (task 3.3) - ============================================================ */ - -.bc-game-root__notation-strip { - position: relative; - width: 100%; - padding: 16px 12px; - user-select: none; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); -} - -.bc-game-root__notation-label { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -/* --- Track (horizontal strip area) --- */ -.bc-game-root__notation-track { - position: relative; - height: 48px; - background: #f8fafc; - border: 2px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; -} - -/* --- Slot blocks --- */ -.bc-game-root__notation-slot { - position: absolute; - top: 0; - height: 100%; -} - -.bc-game-root__notation-slot--onset { - background: rgba(14, 165, 233, 0.08); -} - -.bc-game-root__notation-slot--onset + .bc-game-root__notation-slot--onset { - border-left: 1px solid #e2e8f0; -} - -.bc-game-root__notation-slot--rest { - background: transparent; -} - -.bc-game-root__notation-slot--rest + .bc-game-root__notation-slot--rest { - border-left: 1px dashed #e2e8f0; -} - -/* --- Onset dots --- */ -.bc-game-root__notation-onset-marker { - position: absolute; - top: 50%; - width: 8px; - height: 8px; - border-radius: 50%; - background: #0ea5e9; - border: 1.5px solid #0284c7; - transform: translate(-50%, -50%); - z-index: 2; - pointer-events: none; -} - -/* --- Beat indicator (vertical line) --- */ -.bc-game-root__notation-indicator { - position: absolute; - top: 0; - height: 100%; - width: 2px; - background: #ef4444; - z-index: 10; - pointer-events: none; - transition: left 0.08s linear; -} - -.bc-game-root__notation-indicator::after { - content: ""; - position: absolute; - top: -4px; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 6px solid #ef4444; -} - -/* Hide indicator when idle or done */ -.bc-game-root__notation-strip--idle .bc-game-root__notation-indicator, -.bc-game-root__notation-strip--done .bc-game-root__notation-indicator { - display: none; -} - -/* --- Beat number label --- */ -.bc-game-root__notation-beat-label { - margin-top: 4px; - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 10px; - font-weight: 600; - color: #94a3b8; - text-align: center; - min-height: 1.4em; -} - -/* --- Hit feedback markers --- */ -.bc-game-root__notation-hit-marker { - position: absolute; - top: 50%; - width: 20px; - height: 20px; - border-radius: 50%; - transform: translate(-50%, -50%); - z-index: 5; - font-size: 12px; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; - animation: bc-hit-pop 0.25s ease-out both; -} - -.bc-game-root__notation-hit-marker--perfect { - color: #16a34a; - background: rgba(22, 163, 74, 0.12); - border: 1.5px solid #16a34a; -} - -.bc-game-root__notation-hit-marker--good { - color: #d8a03e; - background: rgba(216, 160, 62, 0.12); - border: 1.5px solid #d8a03e; -} - -.bc-game-root__notation-hit-marker--miss { - color: #ef4444; - background: rgba(239, 68, 68, 0.12); - border: 1.5px solid #ef4444; -} - -.bc-game-root__notation-hit-marker--miss-extra { - color: #94a3b8; - background: rgba(148, 163, 184, 0.12); - border: 1.5px dashed #94a3b8; - opacity: 0.7; -} - -@keyframes bc-hit-pop { - from { - transform: translate(-50%, -50%) scale(0.3); - opacity: 0; - } - to { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } -} - -/* ============================================================ - HUD — Heads-Up Display (task 3.4) - ============================================================ */ - -.bc-game-root__hud { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 8px 12px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); - user-select: none; - position: relative; - min-height: 48px; -} - -/* --- Shared label style --- */ -.bc-game-root__hud-label { - display: block; - font-size: 9px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: #94a3b8; - line-height: 1; -} - -/* --- Score --- */ -.bc-game-root__hud-score-wrap { - display: flex; - flex-direction: column; - align-items: flex-start; - min-width: 72px; -} - -.bc-game-root__hud-score { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 20px; - font-weight: 800; - color: #0f172a; - line-height: 1.2; - transition: color 0.15s ease; -} - -/* --- Lives --- */ -.bc-game-root__hud-lives-wrap { - display: flex; - flex-direction: column; - align-items: center; - min-width: 52px; -} - -.bc-game-root__hud-lives { - display: inline-flex; - gap: 2px; - font-size: 18px; - line-height: 1.2; -} - -.bc-game-root__hud-heart { - color: #ef4444; - transition: transform 0.2s ease, opacity 0.2s ease; -} - -.bc-game-root__hud-heart--empty { - color: #cbd5e1; - opacity: 0.5; -} - -/* --- Streak --- */ -.bc-game-root__hud-streak-wrap { - display: flex; - flex-direction: column; - align-items: center; - min-width: 36px; -} - -.bc-game-root__hud-streak { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 18px; - font-weight: 700; - color: #0ea5e9; - line-height: 1.2; - transition: color 0.15s ease; -} - -/* --- Grade badge --- */ -.bc-game-root__hud-grade { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 14px; - font-weight: 800; - letter-spacing: 0.05em; - padding: 4px 12px; - border-radius: 8px; - white-space: nowrap; - z-index: 20; - pointer-events: none; - animation: bc-grade-flash 0.3s ease-out both; -} - -.bc-game-root__hud-grade--perfect { - color: #16a34a; - background: rgba(22, 163, 74, 0.12); - border: 1.5px solid #16a34a; -} - -.bc-game-root__hud-grade--good { - color: #d8a03e; - background: rgba(216, 160, 62, 0.12); - border: 1.5px solid #d8a03e; -} - -.bc-game-root__hud-grade--miss { - color: #ef4444; - background: rgba(239, 68, 68, 0.12); - border: 1.5px solid #ef4444; -} - -@keyframes bc-grade-flash { - from { - transform: translate(-50%, -50%) scale(0.5); - opacity: 0; - } - 50% { - transform: translate(-50%, -50%) scale(1.15); - opacity: 1; - } - to { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } -} - -/* --- Phase indicator --- */ -.bc-game-root__hud-phase-wrap { - display: flex; - flex-direction: column; - align-items: flex-end; - min-width: 68px; -} - -.bc-game-root__hud-phase { - font-size: 12px; - font-weight: 700; - color: #1e3a52; - letter-spacing: 0.05em; - text-transform: uppercase; - line-height: 1.2; -} - -/* --- BPM --- */ -.bc-game-root__hud-bpm-wrap { - display: flex; - flex-direction: column; - align-items: flex-end; - min-width: 52px; -} - -.bc-game-root__hud-bpm { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 12px; - font-weight: 600; - color: #64748b; - line-height: 1.2; -} - -/* ============================================================ - Run Summary — end-of-run results panel (task 4.1) - ============================================================ */ - -.bc-game-root__run-summary { - display: flex; - flex-direction: column; - gap: 12px; - max-width: 520px; - margin: 0 auto; - padding: 24px; - background: #ffffff; - border: 4px solid #f8fafc; - border-radius: 18px; - box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.45); - transform: rotate(0deg); -} - -/* --- Title --- */ -.bc-game-root__summary-title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 800; - color: #0f172a; - text-align: center; - letter-spacing: -0.02em; - line-height: 1.3; -} - -/* --- Stat rows --- */ -.bc-game-root__summary-stat { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - background: #f8fafc; - border-radius: 10px; - min-height: 40px; -} - -.bc-game-root__summary-stat-label { - font-size: 13px; - font-weight: 600; - color: #64748b; - letter-spacing: 0.02em; -} - -.bc-game-root__summary-stat-value { - font-family: "SF Mono", "Fira Code", "Consolas", monospace; - font-size: 16px; - font-weight: 700; - color: #0f172a; - text-align: right; -} - -/* --- Grade-specific colouring --- */ -.bc-game-root__summary-grade--s .bc-game-root__summary-stat-value { - color: #16a34a; -} - -.bc-game-root__summary-grade--a .bc-game-root__summary-stat-value { - color: #0ea5e9; -} - -.bc-game-root__summary-grade--b .bc-game-root__summary-stat-value { - color: #d8a03e; -} - -.bc-game-root__summary-grade--c .bc-game-root__summary-stat-value { - color: #f97316; -} - -.bc-game-root__summary-grade--d .bc-game-root__summary-stat-value { - color: #ef4444; -} - -/* --- Button group --- */ -.bc-game-root__summary-btns { - display: flex; - gap: 10px; - margin-top: 8px; -} - -.bc-game-root__summary-btn { - flex: 1; - padding: 12px 20px; - border: 4px solid #f8fafc; - border-radius: 12px; - font-size: 14px; - font-weight: 700; - cursor: pointer; - transition: transform 0.1s ease, box-shadow 0.1s ease; - text-align: center; - line-height: 1.2; -} - -.bc-game-root__summary-btn:active { - transform: translateY(1px); -} - -.bc-game-root__summary-btn:focus-visible { - outline: 3px solid #0ea5e9; - outline-offset: 2px; -} - -.bc-game-root__summary-btn--primary { - background: #0ea5e9; - color: #1e3a52; - box-shadow: 3px 4px 0 rgba(0, 0, 0, 0.3); -} - -.bc-game-root__summary-btn--primary:hover { - transform: translateY(-1px); - box-shadow: 4px 5px 0 rgba(0, 0, 0, 0.35); -} - -.bc-game-root__summary-btn--primary:active { - box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.3); -} - -.bc-game-root__summary-btn--secondary { - background: #fdfeff; - color: #1e3a52; - box-shadow: 3px 4px 0 rgba(0, 0, 0, 0.2); -} - -.bc-game-root__summary-btn--secondary:hover { - transform: translateY(-1px); - box-shadow: 4px 5px 0 rgba(0, 0, 0, 0.25); -} - -.bc-game-root__summary-btn--secondary:active { - box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.2); -} From b7eb9705cb4b2dc9eab4e3ba4118fef9a85f53bb Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Mon, 13 Jul 2026 21:31:29 +0200 Subject: [PATCH 2/3] Address review: guitar/bass modifier value, clarify BPM steps The instrument picker now shows 'guitar/bass' (mapped to the internal guitar input path in configFromModifiers); README tempo wording names the 20 BPM increments. Co-Authored-By: Claude Fable 5 --- README.md | 4 ++-- game.js | 7 +++++-- plugin.json | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7c91f67..8098cd1 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ A feedBack minigame plugin for practicing rhythms on your real instrument — gu | Modifier | Values | Default | |---|---|---| -| Instrument | guitar, piano, drums | guitar | +| Instrument | guitar/bass, piano, drums | guitar/bass | | Mode | learning, challenge | learning | -| Tempo (BPM) | 60–160 in steps | 80 | +| Tempo (BPM) | 60–160 in 20 BPM steps | 80 | | Calibration | auto, on, off | auto | ## Structure diff --git a/game.js b/game.js index 75eafcd..1caee15 100644 --- a/game.js +++ b/game.js @@ -52,8 +52,11 @@ function configFromModifiers(modifiers) { modifiers = modifiers || {}; var bpm = parseInt(modifiers.tempo, 10); + // "guitar/bass" is the user-facing modifier value; internally both are + // handled by the guitar (notedetect) input path. + var instrument = modifiers.instrument === "guitar/bass" ? "guitar" : modifiers.instrument; return { - instrument: modifiers.instrument || "guitar", + instrument: instrument || "guitar", mode: modifiers.mode || "learning", bpm: isNaN(bpm) ? 80 : bpm, calibration: modifiers.calibration || "auto", @@ -81,7 +84,7 @@ // Mirrored in plugin.json's minigame block; the manifest copy takes // precedence in the host's modifier picker. modifiers: [ - { id: "instrument", label: "Instrument", default: "guitar", values: ["guitar", "piano", "drums"] }, + { id: "instrument", label: "Instrument", default: "guitar/bass", values: ["guitar/bass", "piano", "drums"] }, { id: "mode", label: "Mode", default: "learning", values: ["learning", "challenge"] }, { id: "tempo", label: "Tempo (BPM)", default: "80", values: ["60", "80", "100", "120", "140", "160"] }, { id: "calibration", label: "Calibration", default: "auto", values: ["auto", "on", "off"] }, diff --git a/plugin.json b/plugin.json index 4e75ecf..b7eb933 100644 --- a/plugin.json +++ b/plugin.json @@ -14,8 +14,8 @@ { "id": "instrument", "label": "Instrument", - "default": "guitar", - "values": ["guitar", "piano", "drums"] + "default": "guitar/bass", + "values": ["guitar/bass", "piano", "drums"] }, { "id": "mode", From 92189bc6ad506ed1023f4d9a6b78df983a3d6d96 Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Mon, 13 Jul 2026 21:39:47 +0200 Subject: [PATCH 3/3] Add regression test for guitar/bass instrument normalization Exposes configFromModifiers via a test-only window hook so the real implementation (not a simulation) is pinned. Co-Authored-By: Claude Fable 5 --- game.js | 3 +++ test/config-from-modifiers.test.js | 42 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 test/config-from-modifiers.test.js diff --git a/game.js b/game.js index 1caee15..f812294 100644 --- a/game.js +++ b/game.js @@ -63,6 +63,9 @@ }; } + // Exposed for tests only — not part of the SDK surface. + window.feedBackMinigamesBongoCatTest = { configFromModifiers: configFromModifiers }; + var controller = null; function startRun(container, config, sdk) { diff --git a/test/config-from-modifiers.test.js b/test/config-from-modifiers.test.js new file mode 100644 index 0000000..8319fe4 --- /dev/null +++ b/test/config-from-modifiers.test.js @@ -0,0 +1,42 @@ +import { describe, it, expect, beforeAll } from "vitest"; + +// Loads the real game.js IIFE (jsdom does not fetch the src/ script tags it +// appends, so only the top-level definitions run) and exercises the actual +// configFromModifiers via the test hook it exposes. +describe("configFromModifiers — instrument normalization", () => { + let configFromModifiers; + + beforeAll(async () => { + await import("../game.js"); + configFromModifiers = window.feedBackMinigamesBongoCatTest.configFromModifiers; + }); + + it('normalizes the "guitar/bass" modifier value to the internal "guitar" id', () => { + expect(configFromModifiers({ instrument: "guitar/bass" }).instrument).toBe("guitar"); + }); + + it('passes "piano" and "drums" through unchanged', () => { + expect(configFromModifiers({ instrument: "piano" }).instrument).toBe("piano"); + expect(configFromModifiers({ instrument: "drums" }).instrument).toBe("drums"); + }); + + it('defaults to "guitar" when the instrument is omitted', () => { + expect(configFromModifiers({}).instrument).toBe("guitar"); + expect(configFromModifiers(undefined).instrument).toBe("guitar"); + }); + + it("keeps the rest of the config intact alongside the normalization", () => { + const config = configFromModifiers({ + instrument: "guitar/bass", + mode: "challenge", + tempo: "120", + calibration: "off", + }); + expect(config).toEqual({ + instrument: "guitar", + mode: "challenge", + bpm: 120, + calibration: "off", + }); + }); +});