From e12fb94ae7e2a0c7fe1db667f0eebd391785ab7d Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 8 Mar 2026 16:23:47 +0000 Subject: [PATCH] feat: omni-morph sprite swap, bullet time showboats, music tuning, server hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Creator omni-morph now generates actual sprite sheets for morphed archetypes - 3 new Creator showboats: bullet time attack, ₿ throne summon, disco dance - Music: subtle tempo shift (+10 BPM max), longer phrases (8/16/24 bars), smoother crossfades, less chaotic hi-hat at high intensity - Server: security headers, body size limit, production error masking, CORS origin warning, graceful shutdown with drain - Payments: atomic consume (eliminates SELECT/UPDATE race), release reverts DB - Fight loop: round events for live TUI, retro displayPrompt - Frontend: pass pubkey in payment/queue requests Co-Authored-By: Claude Opus 4.6 --- frontend/src/composables/useWallet.ts | 2 +- frontend/src/game/FightScene.ts | 102 ++- frontend/src/game/sounds.ts | 862 +++++++++++--------------- frontend/src/pages/JoinBoutPage.vue | 2 +- server/src/app.ts | 20 +- server/src/engine/challenges.ts | 1 + server/src/engine/fight-loop.ts | 19 + server/src/engine/human-responses.ts | 8 + server/src/engine/orchestrator.ts | 6 +- server/src/engine/payments.ts | 25 +- server/src/engine/retro-moves.ts | 2 + server/src/fight-loop-cli.ts | 10 + server/src/index.ts | 20 + 13 files changed, 565 insertions(+), 514 deletions(-) diff --git a/frontend/src/composables/useWallet.ts b/frontend/src/composables/useWallet.ts index 86b21a2..a34baad 100644 --- a/frontend/src/composables/useWallet.ts +++ b/frontend/src/composables/useWallet.ts @@ -140,7 +140,7 @@ export function useWallet() { const invoiceRes = await fetch('/api/payments/create-invoice', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ botId }), + body: JSON.stringify({ botId, pubkey: pubkey.value }), }) if (!invoiceRes.ok) { diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 0df6c61..228c53b 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -6734,9 +6734,24 @@ export async function createFightScene(config: FightSceneConfig) { 'garden_gnome', 'lamp_post', 'broom_man', ] - function applyCreatorOmniMorph(fighter: any, _side: 'a' | 'b'): string { + let omniMorphCounter = 0 + + async function applyCreatorOmniMorph(fighter: any, side: 'a' | 'b'): Promise { const pick = OMNI_MORPH_ARCHETYPES[Math.floor(Math.random() * OMNI_MORPH_ARCHETYPES.length)] + // Generate and load a sprite sheet for the picked archetype + const bot = side === 'a' ? botA : botB + const colors = side === 'a' ? colorsA : colorsB + const morphKey = `omniMorph_${side}_${omniMorphCounter++}` + try { + const morphSheet = generateSpriteSheet(bot.seed, bot.tier, colors.primary, colors.secondary, pick) + await loadSpriteWithTimeout(morphKey, morphSheet, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }) + // Swap the fighter's sprite to the morphed archetype + fighter.use(k.sprite(morphKey, { anim: fighter.curAnim() || 'idle' })) + } catch (err) { + console.warn('[FightScene] omni-morph sprite failed:', err) + } + // Golden aura unique to creator morph const aura = k.add([ k.circle(55), @@ -8259,6 +8274,84 @@ export async function createFightScene(config: FightSceneConfig) { spawnSparks(f.pos.x, hy - 20, 6, '#ffd700') await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad) }, + // 12: Bullet time attack — slow-mo dash with afterimages + golden bullet traces + async (f, hx, hy, _sx, _sy, dir) => { + f.play('attack') + // Darken screen for bullet time + const overlay = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(50)]) + await k.tween(0, 0.35, 0.08, (v) => { overlay.opacity = v }) + // Slow-mo lean back + await k.tween(0, -30, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad) + // Golden bullet traces fly past + for (let i = 0; i < 6; i++) { + const by = hy - 15 - Math.random() * 50 + const bullet = k.add([k.rect(15, 2), k.pos(dir > 0 ? W + 10 : -10, by), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(51), k.anchor('center')]) + const bdir = dir > 0 ? -1 : 1 + k.tween(bullet.pos.x, bullet.pos.x + bdir * (W + 30), 0.35, (v) => { bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() }) + sfxZoomWhoosh() + await k.wait(0.04) + } + await k.wait(0.1) + // Snap upright and dash through + await k.tween(-30, 0, 0.06, (v) => { f.angle = v }) + f.play('special'); sfxZoomWhoosh() + // Dash with afterimages + const endX = hx + dir * 160 + for (let i = 0; i < 6; i++) { + const ghostX = hx + (endX - hx) * (i / 6) + const ghost = k.add([k.rect(16, 30), k.pos(ghostX, hy - 15), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.3 - i * 0.04), k.z(49)]) + k.tween(0.3, 0, 0.4, (v) => { ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() }) + } + await k.tween(f.pos.x, endX, 0.12, (v) => { f.pos.x = v }, k.easings.easeInQuad) + screenFlash('#ffd700', 0.06); sfxCritical(); k.shake(8) + spawnSparks(f.pos.x, hy - 20, 10, '#ffd700') + await k.wait(0.15) + // Zip back + await k.tween(overlay.opacity, 0, 0.1, (v) => { overlay.opacity = v }) + overlay.destroy() + await k.tween(f.pos.x, hx, 0.1, (v) => { f.pos.x = v }, k.easings.easeOutQuad) + }, + // 13: ₿ throne summon — conjure golden throne, sit imperiously, dismiss + async (f, hx, hy) => { + sfxSpecial() + const cs = SF + // Throne pieces + const seat = k.add([k.rect(40 * cs, 8 * cs), k.pos(hx, hy - 5), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)]) + const back = k.add([k.rect(35 * cs, 30 * cs), k.pos(hx, hy - 25 * cs), k.anchor('center'), k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(8)]) + const armL = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx - 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)]) + const armR = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx + 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)]) + const parts = [seat, back, armL, armR] + // Fade in + for (const p of parts) k.tween(0, 0.8, 0.2, (v) => { p.opacity = v }) + spawnSparks(hx, hy - 15, 6, '#ffd700') + await k.wait(0.25) + // Sit + f.play('win') + await k.tween(f.pos.y, hy - 12, 0.12, (v) => { f.pos.y = v }, k.easings.easeInQuad) + spawnEmoteText(hx, f.pos.y - 50, 'kneel', '#ffd700') + await k.wait(0.6) + // Stand and dismiss + await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v }) + for (const p of parts) k.tween(p.opacity, 0, 0.2, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() }) + await k.wait(0.2) + }, + // 14: Disco dance — Creator gets down with golden disco moves + async (f, hx, hy, _sx, _sy, dir) => { + f.play('win') + const colors = ['#ffd700', '#ff8c00', '#ffee88', '#c8a000', '#ffffff', '#ffd700'] + spawnEmoteText(f.pos.x, f.pos.y - 55, '~♪ SATOSHI SHUFFLE ♪~', '#ffd700') + for (let i = 0; i < 10; i++) { + const dx = (i % 2 === 0 ? 25 : -25) + (Math.random() - 0.5) * 15 + f.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'win') + await Promise.all([ + k.tween(f.pos.x, hx + dx, 0.06, (v) => { f.pos.x = v }), + k.tween(f.pos.y, hy - 15, 0.03, (v) => { f.pos.y = v }).then(() => + k.tween(f.pos.y, hy, 0.03, (v) => { f.pos.y = v })), + ]) + spawnSparks(f.pos.x, hy - 10, 2, colors[i % colors.length]) + } + f.pos.x = hx + }, ] // Showboat loop: picks random showboats in sequence while active @@ -9074,8 +9167,9 @@ export async function createFightScene(config: FightSceneConfig) { const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0] if (attacker) { if (isCreatorFighter) { - // Omni-morph: creator transforms into a random archetype - const morphedTo = applyCreatorOmniMorph(attacker, attackerSide) + // Omni-morph: creator transforms into a random archetype (sprite swap) + const originalSpriteKey = attackerSide === 'a' ? 'botA' : 'botB' + const morphedTo = await applyCreatorOmniMorph(attacker, attackerSide) screenFlash('#ffd700', 0.15) sfxZoomWhoosh() sfxSpecial() @@ -9092,6 +9186,8 @@ export async function createFightScene(config: FightSceneConfig) { morphRevert = async () => { destroyMorphOverlays() screenFlash('#c8a000', 0.1) + // Swap sprite back to original Creator + attacker.use(k.sprite(originalSpriteKey, { anim: 'idle' })) attacker.color = safeColor(k, '#ffffff') attacker.opacity = 1 await k.tween(0, 1, 0.2, (t) => { diff --git a/frontend/src/game/sounds.ts b/frontend/src/game/sounds.ts index a7b5d7f..47eea98 100644 --- a/frontend/src/game/sounds.ts +++ b/frontend/src/game/sounds.ts @@ -1583,572 +1583,452 @@ interface MusicTrack { drums: number[] } -// Track 1: "Neon Fury" — Cm, relentless, 225bpm +// === COMPOSED TRACKS — Turrican-style emotionally charged fighting music === +// Hand-composed melodies with sweeping arcs, driving bass, shimmering arps + +// Track 1: "Genesis Block" — Am, 155 BPM — Epic sweeping main theme const track1: MusicTrack = { - name: 'Neon Fury', bpm: 225, + name: 'Genesis Block', bpm: 155, bass: [ - 131,165,196,131, 165,196,220,196, 131,196,220,262, 220,196,165,131, - 208,262,311,208, 262,311,349,311, 196,262,330,262, 311,262,208,196, - 156,208,262,311, 262,208,156,208, 196,262,330,392, 330,262,196,262, - 196,247,294,330, 294,247,196,247, 262,330,392,440, 392,330,262,196, - 131,131,165,196, 220,262,220,196, 208,208,262,311, 349,311,262,208, - 156,156,208,262, 311,349,311,262, 196,196,262,330, 392,440,392,330, - 131,196,262,330, 392,330,262,196, 208,311,415,466, 415,311,262,208, - 196,262,330,392, 440,523,440,392, 262,330,440,523, 587,523,440,330, + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,165,220,165, + 175,0,349,0, 175,0,262,0, 175,0,349,0, 175,262,349,262, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,196, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,0, + 110,0,220,0, 110,0,165,0, 110,0,220,165, 110,165,220,165, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,220,294,220, + 82,0,165,0, 82,0,123,0, 82,0,165,123, 82,123,165,123, + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,0,0,0, ], lead: [ - 523,622,784,622, 523,622,784,1047, 784,622,523,622, 784,1047,784,622, - 831,784,622,523, 622,784,831,1047, 1175,1047,831,784, 622,784,831,1047, - 622,784,831,1047, 831,784,622,784, 831,1047,1175,1319, 1175,1047,831,784, - 784,831,1047,1319, 1175,1047,831,784, 1047,1319,1568,1760, 1568,1319,1047,831, - 523,622,784,1047, 784,622,523,466, 622,784,1047,1319, 1047,784,622,523, - 831,784,622,784, 831,1047,1175,1319, 1175,1047,831,784, 622,784,1047,1175, - 784,1047,1319,1568, 1319,1047,784,622, 1047,1319,1568,1760, 1568,1319,1047,784, - 1047,1319,1568,1760, 2093,1760,1568,1319, 1568,1760,2093,1760, 1568,1319,1047,831, + 659,0,587,523, 494,0,523,587, 659,0,784,880, 784,659,587,523, + 698,0,659,587, 523,0,587,659, 698,0,880,1047, 880,698,659,587, + 523,0,659,784, 880,0,784,659, 1047,0,988,880, 784,659,523,659, + 784,0,880,988, 1175,0,988,880, 784,0,659,587, 523,494,440,0, + 659,0,784,880, 988,0,880,784, 659,0,587,659, 784,880,784,659, + 587,0,698,880, 1047,0,880,698, 1175,0,1047,880, 698,587,698,880, + 659,0,831,988, 1319,0,1175,988, 831,988,1319,831, 988,1319,1661,1319, + 880,0,784,659, 523,0,659,784, 880,0,1319,1047, 880,659,523,440, ], arp: [ - 262,330,392,523, 392,330,262,330, 392,523,662,523, 392,330,262,330, - 415,523,622,831, 622,523,415,523, 622,831,1047,831, 622,523,415,523, - 311,392,466,622, 466,392,311,392, 466,622,831,622, 466,392,311,392, - 392,494,587,784, 587,494,392,494, 587,784,1047,784, 587,494,392,494, - 262,392,523,784, 523,392,262,392, 415,523,622,831, 831,622,523,415, - 311,466,622,831, 622,466,311,466, 392,587,784,1047, 1047,784,587,392, - 262,523,784,1047, 1319,1047,784,523, 415,622,831,1175, 1175,831,622,415, - 392,784,1047,1319, 1568,1319,1047,784, 523,1047,1319,1568, 1760,1568,1319,1047, + 440,523,659,523, 440,523,659,880, 659,880,659,523, 440,659,523,440, + 349,440,523,440, 349,440,523,698, 523,698,523,440, 349,523,440,349, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 196,247,294,247, 196,247,294,392, 294,392,294,247, 196,294,247,196, + 440,523,659,880, 659,880,1047,880, 659,523,440,523, 659,880,659,523, + 294,349,440,349, 294,349,440,587, 440,587,440,349, 294,440,349,294, + 330,415,494,415, 330,415,494,659, 494,659,494,415, 330,494,415,330, + 440,523,659,523, 440,659,523,440, 659,523,440,330, 440,523,659,0, ], - chords: [[131,156,196],[208,262,311],[156,196,233],[196,247,294],[131,156,196],[156,196,233],[208,262,311],[196,247,294]], + chords: [[220,262,330],[175,220,262],[262,330,392],[196,247,294],[220,262,330],[294,349,440],[165,208,247],[220,262,330]], drums: [ - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,4,3, 1,3,1,3, 2,3,1,1, 1,1,4,3, 2,1,1,3, - 1,3,1,3, 2,1,1,3, 1,1,4,1, 2,1,1,1, 1,1,1,3, 2,1,4,1, 2,1,1,1, 2,2,1,1, - 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, 1,1,1,1, 2,1,2,1, - 1,1,1,1, 2,2,4,1, 1,1,1,1, 2,2,4,2, 1,1,2,1, 2,2,2,1, 2,2,2,2, 2,2,2,2, + 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,4,0, + 1,3,0,3, 2,0,3,3, 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,3,3,0, 1,3,0,3, 2,3,4,3, + 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,4,0, + 1,3,1,3, 2,3,3,0, 1,3,1,3, 2,3,3,3, 1,3,1,3, 2,3,1,3, 2,2,2,2, 1,0,0,0, ], } -// Track 2: "Dark Circuit" — Em, relentless grind, 235bpm +// Track 2: "Lightning Strike" — Em, 165 BPM — Fast driving energy const track2: MusicTrack = { - name: 'Dark Circuit', bpm: 235, + name: 'Lightning Strike', bpm: 165, bass: [ - 82,110,82,110, 82,110,147,110, 110,147,110,147, 110,82,110,82, - 98,131,98,131, 98,131,165,131, 131,165,131,165, 131,98,131,98, - 82,82,110,147, 165,147,110,82, 98,98,131,165, 196,165,131,98, - 110,147,165,196, 165,147,110,82, 131,165,196,220, 196,165,131,98, - 82,110,147,196, 147,110,82,110, 98,131,165,220, 165,131,98,131, - 82,110,147,165, 196,165,147,110, 98,131,165,196, 220,196,165,131, - 82,147,196,220, 247,220,196,147, 98,165,220,247, 294,247,220,165, - 110,147,196,247, 294,330,294,247, 82,110,196,247, 330,294,247,196, + 82,0,165,0, 82,0,123,0, 82,0,165,0, 82,123,165,123, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,196, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,147, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,220,294,220, + 82,0,165,0, 82,0,123,0, 82,0,165,123, 82,123,165,123, + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,165,220,165, + 123,0,247,0, 123,0,185,0, 123,0,247,185, 123,185,247,185, + 82,0,165,0, 82,0,123,0, 82,0,165,0, 82,0,0,0, ], lead: [ - 659,784,880,784, 659,784,880,1047, 784,880,1047,880, 784,659,784,880, - 784,880,1047,880, 784,880,1047,1175, 1047,1175,1319,1175, 1047,880,784,880, - 659,784,880,1047, 880,784,659,784, 880,1047,1175,1319, 1175,1047,880,784, - 880,1047,1175,1319, 1175,1047,880,784, 1047,1175,1319,1568, 1319,1175,1047,880, - 659,784,880,1047, 1175,1047,880,784, 784,880,1047,1175, 1319,1175,1047,880, - 880,1047,1175,1047, 880,784,880,1047, 1047,1175,1319,1175, 1047,880,1047,1175, - 659,880,1175,1568, 1175,880,659,880, 784,1047,1319,1760, 1319,1047,784,1047, - 880,1175,1568,1760, 2093,1760,1568,1175, 1047,1319,1760,2093, 1760,1568,1319,1047, + 494,0,659,784, 988,0,784,659, 494,0,659,988, 784,659,494,392, + 523,0,659,784, 1047,0,784,659, 523,0,659,784, 1047,784,659,523, + 784,0,988,1175, 784,0,587,494, 784,0,988,1175, 1319,1175,988,784, + 587,0,740,880, 1175,0,880,740, 587,0,740,880, 1175,880,740,587, + 659,740,784,880, 988,0,880,784, 659,740,784,988, 1319,988,784,659, + 880,0,1047,1319, 880,0,659,523, 880,0,1047,1319, 1175,1047,880,659, + 988,0,1245,1480, 988,0,740,622, 988,1245,1480,1976, 1480,1245,988,740, + 659,0,784,988, 1319,0,988,784, 659,784,988,1319, 1568,1319,988,659, ], arp: [ - 330,392,494,659, 494,392,330,392, 494,659,880,659, 494,392,330,392, - 392,494,587,784, 587,494,392,494, 587,784,1047,784, 587,494,392,494, - 330,494,659,880, 1175,880,659,494, 392,587,784,1047, 1319,1047,784,587, - 494,659,880,1175, 1175,880,659,494, 587,784,1047,1319, 1319,1047,784,587, - 330,392,494,659, 880,659,494,392, 392,494,587,784, 1047,784,587,494, - 330,494,659,880, 659,494,330,494, 392,587,784,1047, 784,587,392,587, - 330,659,880,1175, 1568,1175,880,659, 392,784,1047,1319, 1568,1319,1047,784, - 494,880,1175,1568, 1760,1568,1175,880, 330,880,1175,1760, 2093,1760,1175,880, + 330,392,494,392, 330,392,494,659, 494,659,494,392, 330,494,392,330, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392, + 294,370,440,370, 294,370,440,587, 440,587,440,370, 294,440,370,294, + 330,392,494,659, 494,659,784,659, 494,392,330,392, 494,659,494,392, + 440,523,659,523, 440,523,659,880, 659,880,659,523, 440,659,523,440, + 494,622,740,622, 494,622,740,988, 740,988,740,622, 494,740,622,494, + 330,392,494,392, 330,494,392,330, 494,392,330,247, 330,392,494,0, ], - chords: [[165,196,247],[196,247,294],[131,165,196],[147,175,220],[165,196,247],[131,165,196],[196,247,294],[147,175,220]], + chords: [[165,196,247],[131,165,196],[196,247,294],[147,185,220],[165,196,247],[220,262,330],[247,311,370],[165,196,247]], drums: [ - 1,3,1,3, 2,3,1,1, 1,3,1,3, 2,1,1,3, 1,1,1,3, 2,3,1,1, 1,1,4,1, 2,1,1,1, - 1,3,1,1, 2,1,1,3, 1,1,4,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, 2,1,1,1, 2,2,1,1, - 1,1,1,3, 2,1,1,1, 1,1,1,1, 2,1,4,1, 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, - 1,1,1,1, 2,2,4,1, 1,1,1,1, 2,2,4,2, 1,1,2,2, 2,2,2,1, 2,2,2,2, 2,2,2,2, + 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0, + 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,3,3,3, 2,3,4,3, + 1,0,3,0, 2,0,3,0, 1,3,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,4,0, + 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,3,3, 1,3,1,3, 2,3,1,3, 2,2,2,2, 1,0,0,0, ], } -// Track 3: "Pixel Blitz" — F major, frantic energy, 245bpm +// Track 3: "Cypherpunk" — Dm, 148 BPM — Dark mysterious atmosphere const track3: MusicTrack = { - name: 'Pixel Blitz', bpm: 245, + name: 'Cypherpunk', bpm: 148, bass: [ - 175,220,262,175, 220,262,330,262, 233,294,349,233, 294,349,440,349, - 208,262,330,208, 262,330,392,330, 262,330,392,262, 330,392,440,392, - 175,175,220,262, 330,262,220,175, 233,233,294,349, 440,349,294,233, - 208,208,262,330, 392,330,262,208, 262,330,392,440, 523,440,392,330, - 175,220,262,330, 392,330,262,220, 233,294,349,440, 523,440,349,294, - 208,262,330,392, 440,392,330,262, 262,330,392,440, 523,587,523,440, - 175,262,349,440, 523,440,349,262, 233,349,440,523, 587,523,440,349, - 208,330,440,523, 587,523,440,330, 175,349,440,523, 587,698,587,523, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,220,294,220, + 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,175, + 175,0,349,0, 175,0,262,0, 175,0,349,0, 175,262,349,262, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,196, + 147,0,294,0, 147,0,220,0, 147,0,294,220, 147,220,294,220, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,147, + 110,0,220,0, 110,0,165,0, 110,0,220,165, 110,165,220,165, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,0,0,0, ], lead: [ - 698,784,880,784, 698,784,880,1047, 880,1047,1175,1047, 880,784,698,784, - 784,880,1047,880, 784,880,1047,1175, 1047,1175,1319,1175, 1047,880,784,880, - 698,784,880,1047, 1175,1047,880,784, 880,1047,1175,1319, 1568,1319,1175,1047, - 1047,1175,1319,1568, 1319,1175,1047,880, 1175,1319,1568,1760, 1568,1319,1175,1047, - 698,880,1047,1319, 1047,880,698,880, 784,1047,1175,1568, 1175,1047,784,1047, - 880,1047,1175,1047, 880,784,880,1047, 1175,1319,1568,1319, 1175,1047,1175,1319, - 698,880,1175,1568, 1760,1568,1175,880, 784,1047,1319,1760, 2093,1760,1319,1047, - 880,1175,1568,1760, 2093,1760,1568,1175, 1047,1319,1760,2093, 2349,2093,1760,1319, + 587,0,698,0, 880,0,698,587, 523,0,587,698, 880,698,587,523, + 466,0,587,698, 932,0,698,587, 466,0,587,698, 932,880,698,587, + 698,0,880,1047, 698,0,523,440, 698,0,880,1047, 1175,1047,880,698, + 523,0,659,784, 1047,0,784,659, 523,0,659,784, 1047,932,784,659, + 587,698,880,1175, 1397,0,1175,880, 698,0,587,440, 587,698,880,1175, + 784,0,932,1175, 784,0,587,466, 784,932,1175,1568, 1175,932,784,587, + 880,0,1109,1319, 880,0,659,554, 880,1109,1319,1760, 1319,1109,880,659, + 587,0,698,880, 1175,0,880,698, 587,0,698,880, 1175,880,698,587, ], arp: [ - 349,440,523,698, 523,440,349,440, 523,698,932,698, 523,440,349,440, - 466,587,698,932, 698,587,466,587, 698,932,1175,932, 698,587,466,587, - 415,523,659,880, 1175,880,659,523, 523,659,784,1047, 1319,1047,784,659, - 523,659,784,1047, 1319,1047,784,659, 698,880,1047,1319, 1568,1319,1047,880, - 349,523,698,932, 1175,932,698,523, 466,698,932,1175, 1568,1175,932,698, - 415,659,880,1175, 1568,1175,880,659, 523,784,1047,1319, 1760,1319,1047,784, - 349,698,1047,1319, 1568,1319,1047,698, 466,932,1175,1568, 1760,1568,1175,932, - 523,1047,1319,1760, 2093,1760,1319,1047, 698,1175,1568,2093, 2349,2093,1568,1175, + 294,349,440,349, 294,349,440,587, 440,587,440,349, 294,440,349,294, + 233,294,349,294, 233,294,349,466, 349,466,349,294, 233,349,294,233, + 349,440,523,440, 349,440,523,698, 523,698,523,440, 349,523,440,349, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 294,349,440,587, 440,587,698,587, 440,349,294,349, 440,587,440,349, + 196,233,294,233, 196,233,294,392, 294,392,294,233, 196,294,233,196, + 220,277,330,277, 220,277,330,440, 330,440,330,277, 220,330,277,220, + 294,349,440,349, 294,440,349,294, 440,349,294,220, 294,349,440,0, ], - chords: [[175,220,262],[233,294,349],[208,262,330],[262,330,392],[175,220,262],[208,262,330],[233,294,349],[262,330,392]], + chords: [[147,175,220],[233,294,349],[175,220,262],[131,165,196],[147,175,220],[196,233,294],[220,277,330],[147,175,220]], drums: [ - 1,1,1,3, 2,1,1,3, 1,1,1,3, 2,1,4,1, 1,1,1,3, 2,1,1,1, 1,1,4,1, 2,1,1,1, - 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, 2,1,1,1, 2,2,1,1, - 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, 1,1,1,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, - 1,1,1,1, 2,2,4,1, 1,1,1,1, 2,2,4,2, 1,1,2,2, 2,2,2,2, 2,2,2,2, 2,2,2,2, + 1,0,0,3, 0,0,2,0, 1,0,0,3, 0,0,2,0, 1,0,0,3, 0,0,2,0, 1,0,3,3, 2,0,0,0, + 1,0,0,3, 0,0,2,0, 1,0,3,3, 2,0,0,0, 1,0,0,3, 0,0,2,0, 1,0,0,3, 2,0,4,0, + 1,0,3,3, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,4,0, + 1,0,3,0, 2,0,3,3, 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,1,3, 2,2,4,2, 1,0,0,0, ], } -// Track 4: "Skull Crusher" — Am, insane thrash, 260bpm +// Track 4: "Hash Storm" — Cm, 170 BPM — Intense boss battle energy const track4: MusicTrack = { - name: 'Skull Crusher', bpm: 260, + name: 'Hash Storm', bpm: 170, bass: [ - 110,131,110,131, 110,131,165,131, 131,165,131,165, 131,110,131,110, - 98,110,98,131, 131,165,131,110, 110,131,165,196, 165,131,110,131, - 110,110,131,165, 196,220,196,165, 98,98,131,165, 196,220,196,165, - 110,131,165,196, 220,247,220,196, 98,131,165,196, 220,247,294,247, - 110,131,165,196, 220,196,165,131, 98,110,131,165, 196,165,131,110, - 110,110,131,165, 196,220,247,220, 98,98,131,165, 196,247,294,247, - 110,131,196,247, 294,330,294,247, 98,131,196,247, 330,349,330,247, - 110,165,220,294, 330,349,330,294, 110,165,247,330, 392,349,330,247, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,196, + 104,0,208,0, 104,0,156,0, 104,0,208,0, 104,156,208,156, + 156,0,311,0, 156,0,233,0, 156,0,311,0, 156,233,311,233, + 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,175, + 131,0,262,0, 131,0,196,0, 131,0,262,196, 131,196,262,196, + 175,0,349,0, 175,0,262,0, 175,0,349,0, 175,262,349,262, + 98,0,196,0, 98,0,147,0, 98,0,196,147, 98,147,196,147, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,0,0,0, ], lead: [ - 880,1047,880,1047, 1175,1047,880,1047, 1047,1175,1047,1175, 1319,1175,1047,1175, - 784,880,784,880, 1047,880,784,880, 880,1047,880,1047, 1175,1047,880,1047, - 880,1047,1175,1319, 1175,1047,880,1047, 1047,1175,1319,1568, 1319,1175,1047,1175, - 1175,1319,1568,1760, 1568,1319,1175,1047, 1319,1568,1760,2093, 1760,1568,1319,1175, - 880,1047,1175,1319, 1568,1319,1175,1047, 784,880,1047,1175, 1319,1175,1047,880, - 880,1047,1319,1568, 1319,1047,880,1047, 1047,1175,1568,1760, 1568,1175,1047,1175, - 880,1175,1568,1760, 2093,1760,1568,1175, 1047,1319,1760,2093, 2349,2093,1760,1319, - 1175,1568,2093,2349, 2093,1760,1568,1175, 880,1319,1760,2349, 2637,2349,1760,1319, + 784,0,622,523, 392,0,523,622, 784,0,1047,1245, 1047,784,622,523, + 831,0,622,523, 415,0,523,622, 831,0,1047,1245, 1047,831,622,523, + 622,0,784,932, 1245,0,932,784, 622,0,784,932, 1245,932,784,622, + 932,0,1175,1397, 932,0,698,587, 932,1175,1397,1865, 1397,1175,932,698, + 523,622,784,1047, 1245,0,1047,784, 622,523,622,784, 1047,1245,1568,1245, + 698,0,831,1047, 1397,0,1047,831, 698,0,831,1047, 1397,1047,831,698, + 784,0,988,1175, 1568,0,1175,988, 784,988,1175,1568, 1976,1568,1175,988, + 1047,0,784,622, 523,0,622,784, 1047,0,1245,1568, 1047,784,622,523, ], arp: [ - 220,262,330,440, 587,440,330,262, 196,247,294,392, 523,392,294,247, - 220,330,440,587, 784,587,440,330, 196,294,392,523, 698,523,392,294, - 220,330,440,587, 880,587,440,330, 196,294,392,523, 784,523,392,294, - 220,440,587,880, 1175,880,587,440, 196,392,523,784, 1047,784,523,392, - 220,262,330,440, 587,440,330,262, 196,247,294,392, 523,392,294,247, - 220,330,440,587, 880,587,440,330, 196,294,392,523, 784,523,392,294, - 220,440,880,1175, 1568,1175,880,440, 196,392,784,1047, 1568,1047,784,392, - 220,440,880,1568, 2093,1568,880,440, 220,880,1175,1760, 2349,1760,1175,880, + 262,311,392,311, 262,311,392,523, 392,523,392,311, 262,392,311,262, + 208,262,311,262, 208,262,311,415, 311,415,311,262, 208,311,262,208, + 311,392,466,392, 311,392,466,622, 466,622,466,392, 311,466,392,311, + 233,294,349,294, 233,294,349,466, 349,466,349,294, 233,349,294,233, + 262,311,392,523, 392,523,622,523, 392,311,262,311, 392,523,392,311, + 349,415,523,415, 349,415,523,698, 523,698,523,415, 349,523,415,349, + 392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392, + 262,311,392,311, 262,392,311,262, 392,311,262,196, 262,311,392,0, ], - chords: [[110,131,165],[98,131,147],[131,165,196],[147,175,220],[110,131,165],[131,165,196],[98,131,147],[147,175,220]], + chords: [[262,311,392],[208,262,311],[156,196,233],[233,294,349],[262,311,392],[175,208,262],[196,247,294],[262,311,392]], drums: [ - 1,1,1,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, - 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, 2,1,1,1, 2,2,1,1, - 1,1,1,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, - 1,1,1,1, 2,2,4,2, 1,1,1,1, 2,2,4,2, 2,2,2,2, 2,2,2,2, 2,2,2,2, 2,2,2,2, + 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,4,0, + 1,3,1,3, 2,0,3,3, 1,3,0,3, 2,3,3,0, 1,3,1,3, 2,3,3,0, 1,3,3,3, 2,3,4,3, + 1,3,1,3, 2,3,3,0, 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,0,3,3, 1,3,1,3, 2,3,4,0, + 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,1,3, 1,1,1,1, 2,2,2,2, 2,2,2,2, 1,0,0,0, ], } -// === TRACK GENERATOR === -// Genre types for distinct melodic DNA per style -type Genre = 'funk' | 'hiphop' | 'rock' | 'metal' | 'chiptune' | 'jazz' | 'electronic' | 'latin' | 'reggae' - -// Genre-specific bass patterns (each genre has its own feel) -const GENRE_BASS: Record = { - funk: [ - [0,0,0,4, 0,0,2,0, 3,0,0,5, 0,3,0,2], // syncopated slap bass - [0,0,4,0, 2,0,0,4, 0,3,0,5, 3,0,2,0], - [0,2,0,4, 0,0,5,0, 3,0,4,0, 2,0,0,4], - [0,0,0,0, 4,0,0,2, 0,0,3,0, 5,0,4,0], +// Track 5: "Proof of Work" — Am, 145 BPM — Grinding determination +const track5: MusicTrack = { + name: 'Proof of Work', bpm: 145, + bass: [ + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,165,220,0, + 82,0,165,0, 82,0,123,0, 82,0,165,0, 82,123,165,0, + 87,0,175,0, 87,0,131,0, 87,0,175,0, 87,131,175,0, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,0, + 110,0,220,0, 110,0,165,0, 110,0,220,165, 110,165,220,0, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,220,294,0, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,0, + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,0,0,0, ], - hiphop: [ - [0,0,0,0, 0,0,0,0, 3,0,0,0, 0,0,0,0], // deep sparse hits - [0,0,0,0, 2,0,0,0, 0,0,0,0, 4,0,0,0], - [0,0,0,2, 0,0,0,0, 0,0,3,0, 0,0,0,0], - [0,0,0,0, 0,0,2,0, 0,0,0,0, 0,0,4,2], + lead: [ + 440,0,523,659, 880,0,659,523, 440,0,659,880, 1047,880,659,523, + 659,0,784,988, 659,0,494,392, 659,0,784,988, 1319,988,784,659, + 698,0,880,1047, 698,0,523,440, 698,880,1047,1397, 1047,880,698,523, + 659,0,784,1047, 659,0,523,392, 659,784,1047,1319, 1047,784,659,523, + 880,0,1047,1319, 880,0,659,523, 880,1047,1319,1760, 1319,1047,880,659, + 587,0,698,880, 1175,0,880,698, 587,698,880,1175, 1397,1175,880,698, + 784,0,988,1175, 784,0,587,494, 784,988,1175,1568, 1175,988,784,587, + 880,0,659,523, 440,0,523,659, 880,0,1047,1319, 880,659,523,440, ], - rock: [ - [0,0,0,0, 2,2,0,0, 4,4,0,0, 2,2,0,0], // power chord root pumps - [0,0,2,2, 0,0,4,4, 0,0,5,5, 4,4,2,2], - [0,0,0,0, 0,0,0,0, 4,4,4,4, 2,2,0,0], - [0,2,4,2, 0,2,4,5, 4,2,0,2, 4,5,7,5], + arp: [ + 440,523,659,523, 440,523,659,880, 659,880,659,523, 440,659,523,440, + 330,392,494,392, 330,392,494,659, 494,659,494,392, 330,494,392,330, + 349,440,523,440, 349,440,523,698, 523,698,523,440, 349,523,440,349, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 440,523,659,880, 659,880,1047,880, 659,523,440,523, 659,880,659,523, + 294,349,440,349, 294,349,440,587, 440,587,440,349, 294,440,349,294, + 392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392, + 440,523,659,523, 440,659,523,440, 659,523,440,330, 440,523,659,0, ], - metal: [ - [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0], // tremolo root - [0,0,0,1, 0,0,0,1, 0,0,0,1, 0,0,0,1], // palm mute gallop - [0,0,3,0, 0,0,5,0, 0,0,3,0, 0,0,1,0], // staccato riff - [0,1,0,3, 0,1,0,5, 0,1,0,3, 5,3,1,0], // thrash riff - ], - chiptune: [ - [0,0,2,4, 2,0,2,4, 3,3,5,7, 5,3,2,0], // classic bouncy - [0,2,4,2, 0,4,5,4, 3,5,7,5, 3,2,0,2], - [0,4,7,4, 0,2,5,2, 3,7,10,7, 5,4,2,0], - [0,0,4,7, 4,0,0,4, 3,3,7,10, 7,3,0,3], - ], - jazz: [ - [0,2,4,5, 7,5,4,2, 0,3,5,7, 9,7,5,3], // walking bass - [0,4,7,4, 2,5,9,5, 4,7,11,7, 5,4,2,0], - [0,1,2,3, 4,5,4,3, 2,3,4,5, 7,5,4,2], // chromatic walk - [0,3,7,3, 5,2,7,4, 0,4,7,4, 9,7,4,2], - ], - electronic: [ - [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0], // pulsing single note - [0,0,0,0, 4,4,4,4, 0,0,0,0, 7,7,7,7], // alternating octaves - [0,0,4,0, 0,0,7,0, 0,0,4,0, 0,0,2,0], - [0,4,0,7, 0,4,0,9, 0,4,0,7, 0,9,0,4], - ], - latin: [ - [0,0,0,4, 0,0,2,0, 0,0,0,4, 0,2,0,0], // tresillo bass - [0,0,0,3, 0,0,5,0, 0,0,0,3, 0,5,0,0], - [0,4,0,0, 7,0,0,4, 0,5,0,0, 7,0,4,0], - [0,0,4,0, 0,7,0,4, 0,0,5,0, 0,7,0,5], - ], - reggae: [ - [0,0,0,0, 0,0,0,0, 4,0,0,0, 0,0,0,0], // one-drop bass - [0,0,0,0, 0,0,0,0, 0,0,0,0, 4,0,2,0], - [0,0,0,0, 4,0,0,0, 0,0,0,0, 2,0,0,0], - [0,0,0,0, 0,0,4,0, 0,0,0,0, 0,0,2,0], + chords: [[220,262,330],[165,196,247],[175,220,262],[131,165,196],[220,262,330],[147,175,220],[196,247,294],[220,262,330]], + drums: [ + 1,0,0,3, 2,0,0,3, 1,0,0,3, 2,0,0,3, 1,0,0,3, 2,0,0,3, 1,0,3,0, 2,0,0,0, + 1,0,0,3, 2,0,3,0, 1,0,0,3, 2,0,0,3, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,4,0, + 1,0,3,0, 2,0,3,0, 1,0,0,3, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,4,0, + 1,0,3,0, 2,0,3,3, 1,0,3,3, 2,3,3,3, 1,3,1,3, 2,3,1,3, 2,2,4,0, 1,0,0,0, ], } -// Genre-specific lead patterns -const GENRE_LEAD: Record = { - funk: [ - [0,0,4,0, 0,7,0,4, 0,0,9,0, 0,7,0,0], // wah-wah stabs - [4,0,0,7, 0,0,4,0, 9,0,0,7, 0,0,4,0], - [0,7,0,0, 4,0,0,7, 0,9,0,0, 7,0,4,0], - [0,0,0,4, 7,0,0,0, 0,0,0,9, 7,4,0,0], +// Track 6: "Satoshi's Dream" — Gm, 140 BPM — Beautiful melancholic melody +const track6: MusicTrack = { + name: "Satoshi's Dream", bpm: 140, + bass: [ + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,0, + 156,0,311,0, 156,0,233,0, 156,0,311,0, 156,233,311,0, + 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,0, + 87,0,175,0, 87,0,131,0, 87,0,175,0, 87,131,175,0, + 98,0,196,0, 98,0,147,0, 98,0,196,147, 98,147,196,0, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,0, + 147,0,294,0, 147,0,220,0, 147,0,294,220, 147,220,294,0, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,0,0,0, ], - hiphop: [ - [7,0,0,0, 4,0,0,0, 7,0,0,0, 9,0,0,0], // looping melodic hook - [4,0,7,0, 0,0,4,0, 9,0,7,0, 0,0,4,0], - [0,0,7,0, 0,0,9,0, 0,0,7,0, 0,0,4,0], - [7,4,0,0, 9,7,0,0, 7,4,0,0, 2,4,0,0], + lead: [ + 587,0,784,932, 1175,0,932,784, 587,0,466,587, 784,932,1175,932, + 622,0,784,932, 1245,0,932,784, 622,0,784,932, 1245,1175,932,784, + 932,0,1175,1397, 932,0,698,587, 466,0,587,698, 932,1175,1397,1175, + 698,0,880,1047, 698,0,523,440, 698,880,1047,1397, 1047,880,698,523, + 784,932,1175,1568, 1175,0,932,784, 587,0,466,392, 587,784,932,1175, + 523,0,622,784, 1047,0,784,622, 523,622,784,1047, 1245,1047,784,622, + 587,0,740,880, 1175,0,880,740, 587,740,880,1175, 1480,1175,880,740, + 784,0,587,466, 392,0,466,587, 784,0,932,1175, 784,587,466,392, ], - rock: [ - [0,2,4,7, 9,7,4,2, 0,2,4,7, 11,9,7,4], // pentatonic licks - [7,9,11,9, 7,4,2,4, 7,9,11,14, 11,9,7,4], - [0,4,7,0, 4,7,11,7, 4,0,4,7, 11,14,11,7], - [14,11,9,7, 4,2,0,2, 4,7,9,11, 14,11,9,7], + arp: [ + 196,233,294,233, 196,233,294,392, 294,392,294,233, 196,294,233,196, + 311,392,466,392, 311,392,466,622, 466,622,466,392, 311,466,392,311, + 233,294,349,294, 233,294,349,466, 349,466,349,294, 233,349,294,233, + 349,440,523,440, 349,440,523,698, 523,698,523,440, 349,523,440,349, + 196,233,294,392, 294,392,466,392, 294,233,196,233, 294,392,294,233, + 262,311,392,311, 262,311,392,523, 392,523,392,311, 262,392,311,262, + 294,370,440,370, 294,370,440,587, 440,587,440,370, 294,440,370,294, + 196,233,294,233, 196,294,233,196, 294,233,196,147, 196,233,294,0, ], - metal: [ - [0,0,0,0, 12,11,0,0, 0,0,0,0, 7,5,0,0], // shred bursts - [0,0,12,14, 16,14,12,0, 0,0,7,9, 11,9,7,0], - [14,0,12,0, 11,0,9,0, 7,0,5,0, 4,0,2,0], // descending shred - [0,2,4,7, 0,2,4,9, 0,2,4,11, 12,11,9,7], // ascending runs - ], - chiptune: [ - [0,2,4,7, 4,2,0,2, 4,7,9,7, 4,2,0,-2], - [0,4,7,11, 9,7,4,2, 0,4,9,11, 14,11,9,7], - [7,9,11,14, 11,9,7,4, 9,11,14,16, 14,11,9,7], - [0,2,4,2, 7,4,2,0, 4,7,9,11, 9,7,4,2], - ], - jazz: [ - [0,2,4,5, 7,9,11,9, 7,5,4,2, 0,2,4,7], // bebop lines - [4,5,7,9, 11,9,7,5, 4,2,0,2, 4,7,9,11], - [0,1,2,4, 5,7,9,7, 5,4,2,1, 0,4,7,11], // chromatic approach - [7,9,11,12, 14,12,11,9, 7,5,4,5, 7,9,11,14], - ], - electronic: [ - [0,0,7,0, 0,0,7,0, 0,0,9,0, 0,0,7,0], // filter sweep feel - [0,4,7,4, 0,4,9,4, 0,4,11,4, 0,4,9,4], - [7,7,7,7, 9,9,9,9, 11,11,11,11, 9,9,9,9], // pulsing - [0,7,0,9, 0,11,0,9, 0,7,0,4, 0,7,0,9], - ], - latin: [ - [0,4,7,0, 4,7,9,7, 4,0,4,7, 9,7,4,0], // salsa piano - [7,0,9,0, 7,0,4,0, 7,0,9,0, 11,0,9,0], - [0,2,4,7, 0,2,4,9, 0,2,4,7, 9,7,4,2], - [4,7,9,4, 7,9,11,7, 9,11,14,9, 11,9,7,4], - ], - reggae: [ - [0,0,4,0, 0,0,4,0, 0,0,7,0, 0,0,4,0], // skank chops - [0,0,7,0, 0,0,7,0, 0,0,9,0, 0,0,7,0], - [0,0,4,7, 0,0,4,7, 0,0,4,9, 0,0,4,7], - [0,0,0,4, 0,0,0,7, 0,0,0,4, 0,0,0,2], + chords: [[196,233,294],[156,196,233],[233,294,349],[175,220,262],[196,233,294],[131,156,196],[147,185,220],[196,233,294]], + drums: [ + 1,0,0,0, 2,0,0,3, 1,0,0,0, 2,0,0,0, 1,0,0,3, 2,0,0,0, 1,0,0,3, 2,0,0,0, + 1,0,0,3, 2,0,0,0, 1,0,3,0, 2,0,0,3, 1,0,0,3, 2,0,0,3, 1,0,3,0, 2,0,4,0, + 1,0,3,0, 2,0,0,3, 1,0,0,3, 2,0,0,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0, + 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,3,3, 1,0,3,3, 2,0,3,3, 2,0,4,0, 1,0,0,0, ], } -// Genre-specific arp patterns -const GENRE_ARP: Record = { - funk: [ - [0,4,7,0, 0,4,7,0, 0,4,9,0, 0,4,7,0], - [0,0,4,0, 7,0,4,0, 0,0,9,0, 7,0,4,0], +// Track 7: "Block Height" — Em, 160 BPM — Rising momentum +const track7: MusicTrack = { + name: 'Block Height', bpm: 160, + bass: [ + 82,0,165,0, 82,0,123,0, 82,0,165,0, 82,123,165,0, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,0, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,220,294,0, + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,0, + 82,0,165,0, 82,0,123,0, 82,0,165,123, 82,123,165,0, + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,165,220,0, + 123,0,247,0, 123,0,185,0, 123,0,247,185, 123,185,247,0, + 82,0,165,0, 82,0,123,0, 82,0,165,0, 82,0,0,0, ], - hiphop: [ - [0,0,0,0, 7,0,0,0, 0,0,0,0, 4,0,0,0], // sparse melodic - [0,0,7,0, 0,0,0,0, 0,0,4,0, 0,0,0,0], + lead: [ + 659,0,784,988, 659,0,494,392, 659,784,988,1319, 988,784,659,494, + 784,0,988,1175, 784,0,587,494, 784,988,1175,1568, 1175,988,784,587, + 587,0,740,880, 1175,0,880,740, 587,740,880,1175, 1480,1175,880,740, + 523,0,659,784, 1047,0,784,659, 523,659,784,1047, 1319,1047,784,659, + 659,784,988,1319, 1568,0,1319,988, 784,0,659,784, 988,1319,1568,1319, + 880,0,1047,1319, 880,0,659,523, 880,1047,1319,1760, 1319,1047,880,659, + 988,0,1245,1480, 988,0,740,622, 988,1245,1480,1976, 1480,1245,988,740, + 1319,0,988,784, 659,0,784,988, 1319,0,1568,1976, 1319,988,784,659, ], - rock: [ - [0,4,7,4, 0,4,7,4, 0,4,9,4, 0,4,7,4], // power chord arp - [0,7,4,7, 0,7,4,7, 0,9,4,9, 0,7,4,7], + arp: [ + 330,392,494,392, 330,392,494,659, 494,659,494,392, 330,494,392,330, + 392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392, + 294,370,440,370, 294,370,440,587, 440,587,440,370, 294,440,370,294, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 330,392,494,659, 494,659,784,659, 494,392,330,392, 494,659,494,392, + 440,523,659,523, 440,523,659,880, 659,880,659,523, 440,659,523,440, + 494,622,740,622, 494,622,740,988, 740,988,740,622, 494,740,622,494, + 330,392,494,392, 330,494,392,330, 494,392,330,247, 330,392,494,0, ], - metal: [ - [0,0,7,0, 0,0,7,0, 0,0,7,0, 12,0,7,0], // tremolo picking - [0,7,12,7, 0,7,12,7, 0,7,14,7, 0,7,12,7], - ], - chiptune: [ - [0,2,4,7, 4,2,0,2, 4,7,9,7, 4,2,0,2], - [0,4,7,11, 7,4,0,4, 7,11,14,11, 7,4,0,4], - [0,7,4,11, 7,14,11,7, 4,11,7,14, 11,4,7,0], - ], - jazz: [ - [0,4,7,9, 11,9,7,4, 0,4,7,11, 14,11,7,4], - [0,2,4,7, 9,7,4,2, 0,4,9,11, 9,7,4,0], - ], - electronic: [ - [0,4,7,4, 0,4,7,4, 0,4,7,4, 0,4,7,4], // relentless arp - [0,7,4,11, 0,7,4,11, 0,9,4,11, 0,7,4,11], - [0,4,7,11, 14,11,7,4, 0,4,7,11, 14,11,7,4], - ], - latin: [ - [0,4,7,0, 4,7,9,7, 4,0,4,7, 9,7,4,0], - [0,7,4,7, 0,9,4,9, 0,7,4,7, 0,4,7,4], - ], - reggae: [ - [0,0,4,0, 0,0,7,0, 0,0,4,0, 0,0,2,0], - [0,0,7,0, 0,0,4,0, 0,0,7,0, 0,0,9,0], + chords: [[165,196,247],[196,247,294],[147,185,220],[131,165,196],[165,196,247],[220,262,330],[247,311,370],[165,196,247]], + drums: [ + 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0, + 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,3,3,3, 2,3,4,0, + 1,0,3,0, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,4,0, + 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,3,3, 1,3,1,3, 2,3,1,3, 2,2,4,0, 1,0,0,0, ], } -// Genre-specific chord progressions (scale degrees for triads) -const GENRE_CHORDS: Record = { - funk: [0, 3, 0, 5, 0, 3, 5, 4], // i-iv-i-v groovy - hiphop: [0, 3, 4, 3, 0, 5, 4, 3], // dark minor loops - rock: [0, 5, 3, 4, 0, 5, 3, 4], // I-V-IV power progression - metal: [0, 1, 5, 4, 0, 1, 3, 0], // i-bII-v chromatic - chiptune: [0, 3, 2, 4, 0, 5, 3, 4], // classic game - jazz: [0, 3, 5, 1, 4, 2, 5, 4], // ii-V-I movement - electronic:[0, 4, 5, 4, 0, 3, 5, 3], // trance progression - latin: [0, 3, 4, 5, 0, 3, 4, 5], // son montuno - reggae: [0, 4, 0, 4, 0, 5, 0, 4], // one chord vibes +// Track 8: "Zero Conf" — Fm, 175 BPM — Urgent racing energy +const track8: MusicTrack = { + name: 'Zero Conf', bpm: 175, + bass: [ + 87,0,175,0, 87,0,131,0, 87,0,175,0, 87,131,175,131, + 139,0,277,0, 139,0,208,0, 139,0,277,0, 139,208,277,208, + 104,0,208,0, 104,0,156,0, 104,0,208,0, 104,156,208,156, + 156,0,311,0, 156,0,233,0, 156,0,311,0, 156,233,311,233, + 87,0,175,0, 87,0,131,0, 87,0,175,131, 87,131,175,131, + 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,175, + 131,0,262,0, 131,0,196,0, 131,0,262,196, 131,196,262,196, + 87,0,175,0, 87,0,131,0, 87,0,175,0, 87,0,0,0, + ], + lead: [ + 698,0,831,1047, 698,0,523,415, 698,831,1047,1397, 1047,831,698,523, + 554,0,698,831, 1109,0,831,698, 554,698,831,1109, 1397,1109,831,698, + 831,0,1047,1245, 831,0,622,523, 831,1047,1245,1661, 1245,1047,831,622, + 622,0,784,932, 1245,0,932,784, 622,784,932,1245, 1568,1245,932,784, + 1047,831,698,523, 698,831,1047,1397, 1661,0,1397,1047, 831,698,523,415, + 932,0,1109,1397, 932,0,698,554, 932,1109,1397,1865, 1397,1109,932,698, + 523,0,659,784, 1047,0,784,659, 523,659,784,1047, 1319,1047,784,659, + 698,831,1047,1397, 1661,0,1397,1047, 831,0,698,831, 1047,698,415,349, + ], + arp: [ + 349,415,523,415, 349,415,523,698, 523,698,523,415, 349,523,415,349, + 277,349,415,349, 277,349,415,554, 415,554,415,349, 277,415,349,277, + 415,523,622,523, 415,523,622,831, 622,831,622,523, 415,622,523,415, + 311,392,466,392, 311,392,466,622, 466,622,466,392, 311,466,392,311, + 349,415,523,698, 523,698,831,698, 523,415,349,415, 523,698,523,415, + 466,554,698,554, 466,554,698,932, 698,932,698,554, 466,698,554,466, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 349,415,523,415, 349,523,415,349, 523,415,349,262, 349,415,523,0, + ], + chords: [[175,208,262],[139,175,208],[208,262,311],[156,196,233],[175,208,262],[233,277,349],[131,165,196],[175,208,262]], + drums: [ + 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, 1,3,0,3, 2,0,4,0, + 1,3,1,3, 2,3,3,0, 1,3,0,3, 2,0,3,3, 1,3,1,3, 2,0,3,3, 1,3,3,3, 2,3,4,3, + 1,3,1,3, 2,3,3,0, 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,3,4,0, + 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,3,3, 1,1,1,1, 2,2,2,2, 2,2,4,2, 1,0,0,0, + ], } -function genTrack(name: string, bpm: number, root: number, scaleIntervals: number[], drumStyle: number[], seed: number, genre: Genre = 'chiptune'): MusicTrack { - let s = seed - const rng = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff } - - const allNotes: number[] = [] - for (let oct = -1; oct < 6; oct++) { - for (const semi of scaleIntervals) { - allNotes.push(Math.round(root * Math.pow(2, oct + semi / 12))) - } - } - const sLen = scaleIntervals.length - const note = (degree: number) => { - const idx = Math.max(0, Math.min(allNotes.length - 1, degree + sLen)) - return allNotes[idx] - } - - // Pick genre-specific patterns - const bassPool = GENRE_BASS[genre] - const leadPool = GENRE_LEAD[genre] - const arpPool = GENRE_ARP[genre] - const chordDegs = GENRE_CHORDS[genre] - - // Bass - const bass: number[] = [] - for (let bar = 0; bar < BARS; bar++) { - const prog = bassPool[(bar + Math.floor(rng() * bassPool.length)) % bassPool.length] - const lift = genre === 'metal' ? 0 : Math.floor(bar / 3) - for (let step = 0; step < STEPS; step++) { - const d = prog[step % prog.length] - // Genre-specific spice - if (genre === 'funk' && d === 0 && rng() < 0.15) { - bass.push(note(Math.floor(rng() * 5) + lift)) // ghost notes - } else if (genre === 'metal' && d === 0) { - bass.push(note(0 + lift)) // palm mute on root - } else { - bass.push(note(d + lift)) - } - } - } - - // Lead - const lead: number[] = [] - for (let bar = 0; bar < BARS; bar++) { - const shape = leadPool[(bar + Math.floor(rng() * leadPool.length)) % leadPool.length] - const octShift = sLen + (bar >= 4 ? sLen : 0) - for (let step = 0; step < STEPS; step++) { - const degree = shape[step % shape.length] + octShift - const vary = rng() < 0.2 ? Math.floor(rng() * 3) - 1 : 0 - // Genre-specific: hiphop/reggae have silent steps - if ((genre === 'hiphop' || genre === 'reggae') && shape[step % shape.length] === 0) { - lead.push(0) - } else { - lead.push(note(Math.max(sLen, degree + vary))) - } - } - } - - // Arp - const arp: number[] = [] - for (let bar = 0; bar < BARS; bar++) { - const pat = arpPool[(bar + Math.floor(rng() * arpPool.length)) % arpPool.length] - const shift = Math.floor(sLen * 0.5) + Math.floor(bar / 3) - for (let step = 0; step < STEPS; step++) { - const d = pat[step % pat.length] - if (d === 0 && (genre === 'hiphop' || genre === 'reggae' || genre === 'funk')) { - arp.push(0) - } else { - arp.push(note(d + shift)) - } - } - } - - // Chords - const chords: number[][] = [] - for (const d of chordDegs) { - if (genre === 'jazz') { - chords.push([note(d), note(d + 2), note(d + 4), note(d + 6)]) // 7th chords - } else if (genre === 'rock' || genre === 'metal') { - chords.push([note(d), note(d + 4)]) // power chords (root + 5th) - } else { - chords.push([note(d), note(d + 2), note(d + 4)]) - } - } - - // Drums with genre-specific fills - const drums: number[] = [] - for (let bar = 0; bar < BARS; bar++) { - for (let step = 0; step < STEPS; step++) { - let d = drumStyle[step % drumStyle.length] - // Build: fills in bars 6-7 - if (bar >= 6 && step >= 12 && d === 3) d = 2 - if (bar >= 7 && step >= 14) d = d === 3 ? 2 : d - // Genre-specific density - if (genre === 'metal' && d === 0 && rng() < 0.4) d = 3 // double bass fills - if (genre === 'rock' && bar >= 4 && d === 0 && rng() < 0.25) d = 3 - if (genre === 'funk' && d === 0 && rng() < 0.3) d = 3 // ghost note hats - if (genre === 'hiphop' && d === 0 && rng() < 0.1) d = 3 // sparse hats - if ((genre === 'chiptune' || genre === 'electronic' || genre === 'jazz' || genre === 'latin') && bar >= 4 && d === 0 && rng() < 0.2) d = 3 - drums.push(d) - } - } - - return { name, bpm, bass, lead, arp, chords, drums } +// Track 9: "Mempool" — Dm, 152 BPM — Dark controlled chaos +const track9: MusicTrack = { + name: 'Mempool', bpm: 152, + bass: [ + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,220,294,220, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,147, + 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,175, + 110,0,220,0, 110,0,165,0, 110,0,220,0, 110,165,220,165, + 147,0,294,0, 147,0,220,0, 147,0,294,220, 147,220,294,220, + 87,0,175,0, 87,0,131,0, 87,0,175,0, 87,131,175,131, + 131,0,262,0, 131,0,196,0, 131,0,262,196, 131,196,262,196, + 147,0,294,0, 147,0,220,0, 147,0,294,0, 147,0,0,0, + ], + lead: [ + 587,698,880,587, 698,880,1175,880, 698,587,440,587, 698,880,1175,1397, + 784,0,932,1175, 784,0,587,466, 784,932,1175,1568, 1175,932,784,587, + 932,0,1175,1397, 932,0,698,587, 466,587,698,932, 1175,1397,1865,1397, + 880,0,1109,1319, 880,0,659,554, 880,1109,1319,1760, 1319,1109,880,659, + 587,0,880,1175, 1397,0,1175,880, 698,587,698,880, 1175,1397,1760,1397, + 698,0,880,1047, 698,0,523,440, 698,880,1047,1397, 1760,1397,1047,880, + 523,659,784,1047, 1319,0,1047,784, 659,523,659,784, 1047,1319,1568,1319, + 1175,0,880,698, 587,0,698,880, 1175,0,1397,1760, 1175,880,698,587, + ], + arp: [ + 294,349,440,349, 294,349,440,587, 440,587,440,349, 294,440,349,294, + 196,233,294,233, 196,233,294,392, 294,392,294,233, 196,294,233,196, + 233,294,349,294, 233,294,349,466, 349,466,349,294, 233,349,294,233, + 220,277,330,277, 220,277,330,440, 330,440,330,277, 220,330,277,220, + 294,349,440,587, 440,587,698,587, 440,349,294,349, 440,587,440,349, + 349,440,523,440, 349,440,523,698, 523,698,523,440, 349,523,440,349, + 262,330,392,330, 262,330,392,523, 392,523,392,330, 262,392,330,262, + 294,349,440,349, 294,440,349,294, 440,349,294,220, 294,349,440,0, + ], + chords: [[147,175,220],[196,233,294],[233,294,349],[220,277,330],[147,175,220],[175,220,262],[131,165,196],[147,175,220]], + drums: [ + 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,3,0, + 1,0,3,3, 2,0,3,0, 1,3,3,0, 2,0,3,3, 1,0,3,0, 2,3,3,0, 1,3,3,3, 2,3,4,3, + 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,4,0, + 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,1,3, 2,2,4,0, 1,0,0,0, + ], } -// Drum pattern presets -const DRUMS_HEAVY = [1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,4,3] -const DRUMS_GROOVE = [1,0,3,1, 2,0,3,0, 1,0,3,1, 2,0,3,3] -const DRUMS_CHILL = [1,0,0,3, 0,0,2,0, 0,3,0,0, 2,0,0,3] -const DRUMS_FRANTIC = [1,1,1,3, 2,1,1,1, 1,1,4,1, 2,1,1,1] -const DRUMS_MARCH = [1,0,1,0, 2,0,1,0, 1,0,1,0, 2,0,4,0] -const DRUMS_SWING = [1,0,3,0, 2,3,0,3, 1,0,3,1, 2,0,3,0] -const DRUMS_HALFTIME = [1,0,0,0, 0,0,0,0, 2,0,0,0, 0,0,3,0] -const DRUMS_DNB = [1,0,0,3, 0,0,2,0, 0,3,0,0, 2,3,1,3] +// Track 10: "Halving Day" — Cm, 162 BPM — Triumphant climax +const track10: MusicTrack = { + name: 'Halving Day', bpm: 162, + bass: [ + 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,196, + 156,0,311,0, 156,0,233,0, 156,0,311,0, 156,233,311,233, + 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,175, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,147, + 131,0,262,0, 131,0,196,0, 131,0,262,196, 131,196,262,196, + 104,0,208,0, 104,0,156,0, 104,0,208,0, 104,156,208,156, + 87,0,175,0, 87,0,131,0, 87,0,175,131, 87,131,175,131, + 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,0,0,0, + ], + lead: [ + 523,0,622,784, 1047,0,784,622, 523,0,622,784, 1047,1245,1568,1245, + 622,0,784,932, 1245,0,932,784, 622,784,932,1245, 1568,1245,932,784, + 932,0,1175,1397, 932,0,698,587, 932,1175,1397,1865, 1175,932,698,587, + 784,0,988,1175, 1568,0,1175,988, 784,988,1175,1568, 1976,1568,1175,988, + 1245,1047,784,523, 622,784,1047,1245, 1568,0,1245,1047, 784,622,523,622, + 831,0,1047,1245, 831,0,622,523, 831,1047,1245,1661, 1245,1047,831,622, + 698,0,831,1047, 1397,0,1047,831, 698,831,1047,1397, 1661,1397,1047,831, + 784,988,1175,1568, 1976,0,1568,1175, 988,784,587,784, 988,1175,1568,784, + ], + arp: [ + 262,311,392,311, 262,311,392,523, 392,523,392,311, 262,392,311,262, + 311,392,466,392, 311,392,466,622, 466,622,466,392, 311,466,392,311, + 233,294,349,294, 233,294,349,466, 349,466,349,294, 233,349,294,233, + 392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392, + 262,311,392,523, 392,523,622,523, 392,311,262,311, 392,523,392,311, + 208,262,311,262, 208,262,311,415, 311,415,311,262, 208,311,262,208, + 349,415,523,415, 349,415,523,698, 523,698,523,415, 349,523,415,349, + 392,494,587,494, 392,587,494,392, 587,494,392,294, 392,494,587,0, + ], + chords: [[262,311,392],[156,196,233],[233,294,349],[196,247,294],[262,311,392],[208,262,311],[175,208,262],[196,247,294]], + drums: [ + 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0, + 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,3,3,0, 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,4,3, + 1,3,3,0, 2,0,3,0, 1,0,3,3, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,3,3,0, 2,3,4,0, + 1,3,1,3, 2,3,3,0, 1,3,1,3, 2,3,1,3, 1,1,1,1, 2,2,2,2, 2,2,4,2, 1,0,0,0, + ], +} -// Genre-specific drum patterns -const DRUMS_FUNK = [1,0,3,0, 2,3,0,3, 1,3,0,1, 2,0,3,0] // syncopated ghost notes -const DRUMS_HIPHOP = [1,0,0,3, 0,0,2,0, 1,0,3,0, 0,0,2,3] // boom bap -const DRUMS_ROCK = [1,3,2,3, 1,3,2,3, 1,3,2,3, 1,3,2,4] // driving backbeat -const DRUMS_REGGAE = [0,0,3,0, 2,0,3,0, 0,0,3,0, 2,0,3,0] // one drop -const DRUMS_BREAK = [1,0,1,3, 2,0,0,1, 0,3,1,0, 2,3,0,1] // chopped breakbeat -const DRUMS_LATIN = [1,0,0,1, 0,0,1,0, 1,0,0,1, 0,1,0,0] // tresillo -const DRUMS_SHUFFLE = [1,3,0,3, 2,0,3,3, 1,3,0,3, 2,3,4,3] // swung triplet feel - -// Scale presets (semitone intervals) -const SCALE_MINOR = [0,2,3,5,7,8,10] -const SCALE_MAJOR = [0,2,4,5,7,9,11] -const SCALE_DORIAN = [0,2,3,5,7,9,10] -const SCALE_BLUES = [0,3,5,6,7,10] -const SCALE_PHRYGIAN = [0,1,3,5,7,8,10] -const SCALE_MIXOLYDIAN = [0,2,4,5,7,9,10] -const SCALE_HARMMINOR = [0,2,3,5,7,8,11] -const SCALE_PENTATONIC = [0,2,4,7,9] -const SCALE_JAPANESE = [0,1,5,7,8] -const SCALE_ARABIC = [0,1,4,5,7,8,11] - -// === FUNK tracks — syncopated slap bass, wah stabs, ghost note drums === -const track5 = genTrack('Funk Machine', 200, 147, SCALE_MIXOLYDIAN, DRUMS_FUNK, 420, 'funk') -const track6 = genTrack('Funk Royale', 195, 131, SCALE_MIXOLYDIAN, DRUMS_FUNK, 1971, 'funk') -const track7 = genTrack('Soul Slugger', 185, 117, SCALE_DORIAN, DRUMS_FUNK, 1968, 'funk') -const track8 = genTrack('Funky Fists', 205, 208, SCALE_MIXOLYDIAN, DRUMS_FUNK, 1975, 'funk') -const track9 = genTrack('Slap City', 210, 165, SCALE_BLUES, DRUMS_FUNK, 1979, 'funk') - -// === HIP HOP tracks — deep sparse bass, looping hooks, boom bap === -const track10 = genTrack('Boom Bap Brawl', 170, 110, SCALE_MINOR, DRUMS_HIPHOP, 1994, 'hiphop') -const track11 = genTrack('Trap Arena', 155, 185, SCALE_MINOR, DRUMS_HIPHOP, 2012, 'hiphop') -const track12 = genTrack('Hip Hop Havoc', 175, 131, SCALE_MINOR, DRUMS_HIPHOP, 1988, 'hiphop') -const track13 = genTrack('Beat Down Blvd', 165, 147, SCALE_DORIAN, DRUMS_HIPHOP, 1996, 'hiphop') -const track14 = genTrack('Street Cypher', 180, 98, SCALE_BLUES, DRUMS_HIPHOP, 2004, 'hiphop') - -// === ROCK tracks — power chord pumps, pentatonic licks, driving backbeat === -const track15 = genTrack('Stadium Rock', 210, 165, SCALE_MAJOR, DRUMS_ROCK, 1985, 'rock') -const track16 = genTrack('Garage Smasher', 220, 147, SCALE_MINOR, DRUMS_ROCK, 1969, 'rock') -const track17 = genTrack('Punk Blitz', 245, 165, SCALE_MAJOR, DRUMS_ROCK, 1977, 'rock') -const track18 = genTrack('Arena Anthem', 200, 131, SCALE_PENTATONIC, DRUMS_ROCK, 1987, 'rock') -const track19 = genTrack('Riff Rampage', 215, 110, SCALE_BLUES, DRUMS_ROCK, 1991, 'rock') - -// === METAL tracks — palm mute gallops, shred bursts, double bass fills === -const track20 = genTrack('Metal Mayhem', 260, 82, SCALE_PHRYGIAN, DRUMS_FRANTIC, 666, 'metal') -const track21 = genTrack('Boss Battle', 240, 147, SCALE_HARMMINOR, DRUMS_HEAVY, 256, 'metal') -const track22 = genTrack('Viking Raid', 225, 110, SCALE_MINOR, DRUMS_MARCH, 793, 'metal') -const track23 = genTrack('Samurai Storm', 245, 123, SCALE_JAPANESE, DRUMS_HEAVY, 1603, 'metal') -const track24 = genTrack('Skull Crusher', 270, 98, SCALE_PHRYGIAN, DRUMS_FRANTIC, 999, 'metal') - -// === CHIPTUNE tracks — classic bouncy game music === -const track25 = genTrack('Retro Arcade', 195, 196, SCALE_MAJOR, DRUMS_GROOVE, 137, 'chiptune') -const track26 = genTrack('Chill Lounge', 155, 131, SCALE_MAJOR, DRUMS_CHILL, 42, 'chiptune') -const track27 = genTrack('Space Opera', 165, 208, SCALE_MAJOR, DRUMS_HALFTIME, 2001, 'chiptune') -const track28 = genTrack('Haunted Circus', 200, 117, SCALE_HARMMINOR, DRUMS_MARCH, 1313, 'chiptune') -const track29 = genTrack('Cyber Punk', 250, 117, SCALE_MINOR, DRUMS_FRANTIC, 99, 'chiptune') - -// === JAZZ tracks — walking bass, bebop lines, 7th chords, swing drums === -const track30 = genTrack('Jazz Fusion', 175, 156, SCALE_DORIAN, DRUMS_SWING, 333, 'jazz') -const track31 = genTrack('Shuffle Beatdown', 190, 196, SCALE_BLUES, DRUMS_SHUFFLE, 1955, 'jazz') -const track32 = genTrack('Smooth Operator', 165, 175, SCALE_DORIAN, DRUMS_SWING, 1961, 'jazz') - -// === ELECTRONIC tracks — pulsing bass, filter sweeps, relentless arps === -const track33 = genTrack('Synthwave Dream', 170, 175, SCALE_MINOR, DRUMS_HALFTIME, 1984, 'electronic') -const track34 = genTrack('Drum & Bass', 255, 196, SCALE_MINOR, DRUMS_DNB, 174, 'electronic') -const track35 = genTrack('Neon Overload', 230, 131, SCALE_MINOR, DRUMS_FRANTIC, 2077, 'electronic') -const track36 = genTrack('Breakbeat Fury', 240, 117, SCALE_MINOR, DRUMS_BREAK, 1997, 'electronic') - -// === LATIN tracks — tresillo bass, salsa piano, clave rhythms === -const track37 = genTrack('Latin Knockout', 200, 131, SCALE_HARMMINOR, DRUMS_LATIN, 1959, 'latin') -const track38 = genTrack('Acid Meltdown', 235, 110, SCALE_ARABIC, DRUMS_LATIN, 303, 'latin') - -// === REGGAE tracks — one-drop bass, skank chops === -const track39 = genTrack('Dub Smash', 170, 196, SCALE_MINOR, DRUMS_REGGAE, 1973, 'reggae') -const track40 = genTrack('Reggae Rumble', 195, 156, SCALE_MAJOR, DRUMS_REGGAE, 1978, 'reggae') - -// === GENRE BLENDS — unique crossovers === -const track41 = genTrack('Western Duel', 190, 165, SCALE_BLUES, DRUMS_SHUFFLE, 1865, 'rock') -const track42 = genTrack('Tropical Storm', 185, 131, SCALE_MIXOLYDIAN, DRUMS_GROOVE, 808, 'funk') -const track43 = genTrack('Disco Inferno', 215, 117, SCALE_MAJOR, DRUMS_GROOVE, 1977, 'funk') - -const ALL_TRACKS = [ - track1, track2, track3, track4, track5, track6, track7, track8, track9, track10, - track11, track12, track13, track14, track15, track16, track17, track18, track19, track20, - track21, track22, track23, track24, track25, track26, track27, track28, track29, track30, - track31, track32, track33, track34, track35, track36, track37, track38, track39, track40, - track41, track42, track43, -] +const ALL_TRACKS = [track1, track2, track3, track4, track5, track6, track7, track8, track9, track10] let activeTrack: MusicTrack = track1 let barIndex = 0 + function playMusicBar() { if (!musicPlaying || !musicGain) return const c = getCtx() getMusicDelay() // ensure delay is created - // Dynamic tempo: base BPM shifts with intensity (+30 BPM at max) - const dynamicBpm = activeTrack.bpm + currentIntensity * 30 + // Dynamic tempo: subtle intensity shift (+10 BPM at max) + const dynamicBpm = activeTrack.bpm + currentIntensity * 10 const beat = 60 / dynamicBpm const now = c.currentTime + 0.05 const bar = barIndex % BARS const off = bar * STEPS const step = beat / 2 - // Switch tracks aggressively — every 2-4 bars, always to a DIFFERENT track - const chillTracks = ALL_TRACKS.filter(t => t.bpm < 185) - const midTracks = ALL_TRACKS.filter(t => t.bpm >= 185 && t.bpm < 230) - const intenseTracks = ALL_TRACKS.filter(t => t.bpm >= 230) - const switchEvery = currentIntensity > 0.7 ? 2 : currentIntensity > 0.4 ? 3 : 4 + // Switch tracks at musical boundaries (full 8-bar cycles) + const chillTracks = ALL_TRACKS.filter(t => t.bpm < 160) + const midTracks = ALL_TRACKS.filter(t => t.bpm >= 160 && t.bpm < 175) + const intenseTracks = ALL_TRACKS.filter(t => t.bpm >= 175) + const switchEvery = currentIntensity > 0.7 ? 8 : currentIntensity > 0.4 ? 16 : 24 if (barIndex > 0 && barIndex % switchEvery === 0) { const pickFrom = (pool: MusicTrack[]) => { const others = pool.filter(t => t !== activeTrack) @@ -2165,12 +2045,12 @@ function playMusicBar() { // Full random for maximum variety activeTrack = pickFrom(ALL_TRACKS) } - // Smooth crossfade: brief gain dip on track switch to prevent abrupt transition + // Smooth crossfade on track switch if (activeTrack !== prevTrack && musicGain) { const curVol = musicGain.gain.value musicGain.gain.setValueAtTime(curVol, now) - musicGain.gain.linearRampToValueAtTime(curVol * 0.4, now + 0.05) - musicGain.gain.linearRampToValueAtTime(curVol, now + 0.15) + musicGain.gain.linearRampToValueAtTime(curVol * 0.25, now + 0.08) + musicGain.gain.linearRampToValueAtTime(curVol, now + 0.4) } } @@ -2203,9 +2083,9 @@ function playMusicBar() { if (d === 3) hihat(musicGain, t, false) if (d === 4 && currentIntensity > 0.4) hihat(musicGain, t, true) - // Extra percussion at high intensity - if (currentIntensity > 0.8 && i % 2 === 0 && Math.random() < 0.3) { - hihat(musicGain, t, false) + // Extra open hihat at high intensity (musical, not chaotic) + if (currentIntensity > 0.85 && i === 14 && Math.random() < 0.3) { + hihat(musicGain, t, true) } } diff --git a/frontend/src/pages/JoinBoutPage.vue b/frontend/src/pages/JoinBoutPage.vue index 42f5336..f62945a 100644 --- a/frontend/src/pages/JoinBoutPage.vue +++ b/frontend/src/pages/JoinBoutPage.vue @@ -366,7 +366,7 @@ async function fightRanked() { const res = await fetch(`/api/queue/join-ranked/${bot.value.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ paymentId }), + body: JSON.stringify({ paymentId, pubkey: pubkey.value }), }) if (res.ok) { const data = await res.json() diff --git a/server/src/app.ts b/server/src/app.ts index 48f62e7..a0b0679 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,6 +1,8 @@ import { Hono } from 'hono' import { cors } from 'hono/cors' import { logger } from 'hono/logger' +import { secureHeaders } from 'hono/secure-headers' +import { bodyLimit } from 'hono/body-limit' import { botsRouter } from './routes/bots.js' import { fightsRouter } from './routes/fights.js' import { queueRouter } from './routes/queue.js' @@ -20,7 +22,8 @@ export const app = new Hono() app.onError((err, c) => { console.error('[botfights] ERROR:', err.message, err.stack) - return c.json({ error: err.message }, 500) + const msg = process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message + return c.json({ error: msg }, 500) }) app.use('*', logger()) @@ -28,6 +31,21 @@ app.use('*', logger()) const allowedOrigin = process.env.CORS_ORIGIN || '*' app.use('/api/*', cors({ origin: allowedOrigin })) +// Security headers: X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy, etc. +app.use('*', secureHeaders({ + contentSecurityPolicy: process.env.NODE_ENV === 'production' ? { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'blob:'], + connectSrc: ["'self'"], + fontSrc: ["'self'"], + } : undefined, +})) + +// Body size limit: 256KB max for API requests (prevents OOM) +app.use('/api/*', bodyLimit({ maxSize: 256 * 1024 })) + // Rate limit all POST endpoints (60/min per IP) app.use('/api/*', rateLimit(60_000, 60)) diff --git a/server/src/engine/challenges.ts b/server/src/engine/challenges.ts index 8b3bc32..26353f8 100644 --- a/server/src/engine/challenges.ts +++ b/server/src/engine/challenges.ts @@ -7,6 +7,7 @@ export interface Challenge { timeout_ms: number scoring: 'factual' | 'creative' baseDamage: number + displayPrompt?: string } interface PromptEntry { diff --git a/server/src/engine/fight-loop.ts b/server/src/engine/fight-loop.ts index cd6633f..1dafa16 100644 --- a/server/src/engine/fight-loop.ts +++ b/server/src/engine/fight-loop.ts @@ -1,6 +1,7 @@ import { db, schema } from '../db/index.js' import { runMockFight } from './mock.js' import { eq } from 'drizzle-orm' +import { fightEvents } from './events.js' export interface FightResult { fightId: string @@ -23,6 +24,7 @@ export interface FightLoopOptions { matchmakingStyle?: 'random' | 'elo_close' | 'mixed' onFightStart?: (botAName: string, botAElo: number, botBName: string, botBElo: number) => void onFightComplete?: (result: FightResult) => void + onRoundComplete?: (data: { round: number; hp: { a: number; b: number }; challengeType: string; winnerId: string | null }) => void onError?: (err: Error) => void } @@ -33,6 +35,7 @@ export async function startFightLoop(options: FightLoopOptions = {}): Promise void) | undefined + if (onRoundComplete) { + unsub = fightEvents.onAll((event) => { + if (event.type === 'round_end') { + onRoundComplete({ + round: event.data.round as number, + hp: event.data.hp as { a: number; b: number }, + challengeType: (event.data.result as any)?.challengeType || '', + winnerId: (event.data.result as any)?.winnerId || null, + }) + } + }) + } + const fightId = await runMockFight(botA.id, botB.id) + unsub?.() // Fetch result const fight = await db.select({ diff --git a/server/src/engine/human-responses.ts b/server/src/engine/human-responses.ts index 1ec93b5..17b13bd 100644 --- a/server/src/engine/human-responses.ts +++ b/server/src/engine/human-responses.ts @@ -18,6 +18,14 @@ interface PendingChallenge { const pending = new Map() +/** Clear all pending human challenges (used during graceful shutdown) */ +export function clearAllPending(): void { + for (const [key, entry] of pending) { + clearTimeout(entry.timeoutHandle) + pending.delete(key) + } +} + const DEFAULT_HUMAN_TIMEOUT_MS = 8_000 export function isHumanPlayer(webhookUrl: string): boolean { diff --git a/server/src/engine/orchestrator.ts b/server/src/engine/orchestrator.ts index 7977359..eebb765 100644 --- a/server/src/engine/orchestrator.ts +++ b/server/src/engine/orchestrator.ts @@ -37,6 +37,10 @@ const MAX_RESPONSE_BYTES = 10 * 1024 // 10KB // Track bots currently in a fight to prevent concurrent fights const activeFighters = new Set() +export function getActiveFighterCount(): number { + return activeFighters.size +} + export function isInFight(botId: string): boolean { return activeFighters.has(botId) } @@ -384,7 +388,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec fightId, roundNumber: round, challengeType: challenge.type, - challengeData: JSON.stringify({ prompt: challenge.prompt, scoring: challenge.scoring, retroKnown: challenge.type === 'retro_mode' ? challenge.answers : undefined }), + challengeData: JSON.stringify({ prompt: challenge.prompt, displayPrompt: challenge.displayPrompt, scoring: challenge.scoring, retroKnown: challenge.type === 'retro_mode' ? challenge.answers : undefined }), botAResponse: responseA.answer, botATimeMs: responseA.timeMs, botAScore: result.botAScore, diff --git a/server/src/engine/payments.ts b/server/src/engine/payments.ts index bec62be..01926f6 100644 --- a/server/src/engine/payments.ts +++ b/server/src/engine/payments.ts @@ -1,5 +1,5 @@ import { nanoid } from 'nanoid' -import { db, schema } from '../db/index.js' +import { db, schema, sqlite } from '../db/index.js' import { eq, and, isNull, sql } from 'drizzle-orm' import { finalizeEvent, getPublicKey } from 'nostr-tools' import * as nip04 from 'nostr-tools/nip04' @@ -643,22 +643,13 @@ export async function consumePaymentForQueue(paymentId: string, botId: string): // Fast path: already consumed in this server lifetime if (consumedPayments.has(paymentId)) return false - // Verify payment belongs to this bot, is confirmed, inbound, and not linked to a fight - const rows = await db.select({ - id: schema.payments.id, - botId: schema.payments.botId, - status: schema.payments.status, - direction: schema.payments.direction, - fightId: schema.payments.fightId, - }).from(schema.payments).where(eq(schema.payments.id, paymentId)).limit(1) - - if (rows.length === 0) return false - const p = rows[0] - if (p.botId !== botId) return false - if (p.status !== 'confirmed') return false - if (p.direction !== 'in') return false - if (p.fightId !== null) return false // already linked to a fight + // Atomic: only marks as consumed if ALL conditions met in a single UPDATE + // Eliminates race window between SELECT check and later UPDATE + const result = sqlite.prepare( + `UPDATE payments SET status = 'consumed' WHERE id = ? AND bot_id = ? AND status = 'confirmed' AND direction = 'in' AND fight_id IS NULL` + ).run(paymentId, botId) + if (result.changes === 0) return false consumedPayments.add(paymentId) return true } @@ -680,6 +671,8 @@ export async function linkPaymentsToFight(fightId: string, paymentIds: string[]) */ export function releasePayment(paymentId: string): void { consumedPayments.delete(paymentId) + // Revert DB status so the payment can be re-consumed or refunded + sqlite.prepare(`UPDATE payments SET status = 'confirmed' WHERE id = ? AND status = 'consumed'`).run(paymentId) } export { ENTRY_FEE_SATS, POT_SATS } diff --git a/server/src/engine/retro-moves.ts b/server/src/engine/retro-moves.ts index a263974..09881be 100644 --- a/server/src/engine/retro-moves.ts +++ b/server/src/engine/retro-moves.ts @@ -122,12 +122,14 @@ export function generateRetroChallenge(): Challenge { const moveList = knownMoves.map(m => ` ${m.input} = ${m.name} (${m.damage} dmg)`).join('\n') + const displayPrompt = 'Binary Gamepads at the ready, GO!' const prompt = `ARCADE ROUND! Pick 3 moves:\n${moveList}\n\nSecret combos exist — longer chains hit harder.\nAnswer: combo1 | combo2 | combo3` return { type: 'retro_mode', label: 'Retro Mode', prompt, + displayPrompt, answers: knownInputs, timeout_ms: 12000, scoring: 'factual', diff --git a/server/src/fight-loop-cli.ts b/server/src/fight-loop-cli.ts index bccc422..28a817c 100644 --- a/server/src/fight-loop-cli.ts +++ b/server/src/fight-loop-cli.ts @@ -79,6 +79,16 @@ async function main() { renderer.render() }, + onRoundComplete: (data) => { + if (state.currentFight) { + state.currentFight.round = data.round + state.currentFight.botA.hp = data.hp.a + state.currentFight.botB.hp = data.hp.b + if (data.challengeType) state.currentFight.challengeType = data.challengeType + renderer.render() + } + }, + onFightComplete: async (result) => { state.completed++ diff --git a/server/src/index.ts b/server/src/index.ts index b173ccd..935b40b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,6 +3,8 @@ import { app } from './app.js' import { runMigrations } from './db/startup.js' import { seedMockBots, seedClassicBots } from './engine/mock.js' import { startBackgroundFights } from './engine/background.js' +import { getActiveFighterCount } from './engine/orchestrator.js' +import { clearAllPending } from './engine/human-responses.js' // Production env validation — warn but don't crash (wallet features degrade gracefully) if (process.env.NODE_ENV === 'production') { @@ -11,6 +13,9 @@ if (process.env.NODE_ENV === 'production') { if (missing.length > 0) { console.warn(`[botfights] WARNING: Missing env vars: ${missing.join(', ')} — wallet/payment features disabled`) } + if (!process.env.CORS_ORIGIN || process.env.CORS_ORIGIN === '*') { + console.warn('[botfights] WARNING: CORS_ORIGIN not set — allowing all origins') + } } // Run migrations and seed mock bots before starting the server @@ -26,3 +31,18 @@ serve({ fetch: app.fetch, port }, () => { // Start background fight loop so the site always has fresh activity startBackgroundFights() }) + +// Graceful shutdown — drain in-flight fights before exiting +const shutdown = async (signal: string) => { + console.log(`[botfights] ${signal} received, shutting down...`) + const active = getActiveFighterCount() + if (active > 0) { + console.log(`[botfights] ${active} fighters still active, waiting 10s...`) + } + await new Promise(r => setTimeout(r, active > 0 ? 10_000 : 500)) + clearAllPending() + console.log('[botfights] shutdown complete') + process.exit(0) +} +process.on('SIGTERM', () => shutdown('SIGTERM')) +process.on('SIGINT', () => shutdown('SIGINT'))