From 5ee6b6c88c6cfdc91a3d5d857b3dcbc7e934317d Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 08:21:16 +0000 Subject: [PATCH] fix: fight restart bug, mobile TTS, character sizing, bitcoin symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix fight replay showing empty scene: validate rounds before playing, retry loading fight data up to 5 times when rounds are missing - Add COOP/COEP headers to production server for SharedArrayBuffer (required by Kokoro TTS WASM threading on mobile) - Add blob: to scriptSrc/workerSrc CSP for Web Worker support - Fix mobile character cutoff: raise ground line to 0.82, add MOBILE_SCALE_CAP (0.55) so fighters never shrink too small - Add ₿ bitcoin chest badge to all fighter sprites (scales with tier) - Add sats coin stack on bot profile page proportional to satsWon - Seed dev tournament with 8 mock bots on startup (dev mode only) Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/FightViewer.vue | 5 +++ frontend/src/game/FightScene.ts | 4 +- frontend/src/game/fight/config.ts | 3 +- frontend/src/game/sprites/index.ts | 35 ++++++++++++---- frontend/src/pages/BotProfilePage.vue | 25 ++++++++++-- frontend/src/pages/FightPage.vue | 7 +++- server/src/app.ts | 10 ++++- server/src/engine/challenges-extra.ts | 8 ++++ server/src/engine/dev-seed.ts | 54 +++++++++++++++++++++++++ server/src/index.ts | 6 +++ 10 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 server/src/engine/dev-seed.ts diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue index 19ccfd9..da62e76 100644 --- a/frontend/src/components/FightViewer.vue +++ b/frontend/src/components/FightViewer.vue @@ -322,6 +322,11 @@ async function replay() { async function _doReplay() { if (!props.fight.botA || !props.fight.botB) return + if (!props.fight.rounds || props.fight.rounds.length === 0) { + console.warn('[FightViewer] No rounds to play — skipping replay') + emit('replay-done') + return + } displayHpA.value = 100 displayHpB.value = 100 logItems.value = [] diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 97f19d6..d6779a4 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -9,7 +9,7 @@ import { pickChoreography } from './fight/choreography/picker' export { pickChoreography } import { isPerfMode, perfParticles, isReducedMotion, haptic, - GROUND_Y_RATIO, HOME_A_RATIO, HOME_B_RATIO, REFERENCE_HEIGHT, SPRITE_LOAD_TIMEOUT_MS, + GROUND_Y_RATIO, HOME_A_RATIO, HOME_B_RATIO, REFERENCE_HEIGHT, MOBILE_SCALE_CAP, SPRITE_LOAD_TIMEOUT_MS, FIGHTER_BASE_SCALE, FIGHTER_TIER_SCALE, JUDGE_SCALE, HUMAN_COACH_SCALE, HUMAN_RUN_SCALE, AMBIENT_PARTICLE_COUNT, SKY_GRADIENT_BANDS, GROUND_DEPTH_LINES, PERSPECTIVE_GRID_LINES, PILLAR_WIDTH, SPEECH_BUBBLE_MAX_CHARS, SPEECH_BUBBLE_WRAP, SPEECH_BUBBLE_MAX_LINES, SPEECH_BUBBLE_FONT_SIZE, SPEECH_BUBBLE_DEFAULT_DURATION, @@ -193,7 +193,7 @@ export async function createFightScene(config: FightSceneConfig) { const vhsTracking = (duration: number = 0.4) => _vhsTracking(fightCtx, duration) const dimensionalShift = (duration: number = 0.6) => _dimensionalShift(fightCtx, duration) - const SF = Math.min(1, H / REFERENCE_HEIGHT) + const SF = Math.max(MOBILE_SCALE_CAP, Math.min(1, H / REFERENCE_HEIGHT)) const GROUND_Y = H * GROUND_Y_RATIO const HOME_A = W * HOME_A_RATIO const HOME_B = W * HOME_B_RATIO diff --git a/frontend/src/game/fight/config.ts b/frontend/src/game/fight/config.ts index e57dfc5..3cbb8bb 100644 --- a/frontend/src/game/fight/config.ts +++ b/frontend/src/game/fight/config.ts @@ -87,10 +87,11 @@ export const HYPERDETAIL_BASE_CHANCE = 0.05 // Base hyperdetail chance export const HYPERDETAIL_INTENSITY_SCALE = 0.45 // Scales with intensity // --- Scene layout --- -export const GROUND_Y_RATIO = 0.78 // Ground line as fraction of canvas height +export const GROUND_Y_RATIO = 0.82 // Ground line as fraction of canvas height export const HOME_A_RATIO = 0.28 // Fighter A home X position ratio export const HOME_B_RATIO = 0.72 // Fighter B home X position ratio export const REFERENCE_HEIGHT = 500 // Reference canvas height for scale factor +export const MOBILE_SCALE_CAP = 0.55 // Cap SF on small screens so fighters aren't too tiny export const SPRITE_LOAD_TIMEOUT_MS = 5_000 // Max wait for sprite loading (mobile hang prevention) // --- Fighter scaling --- diff --git a/frontend/src/game/sprites/index.ts b/frontend/src/game/sprites/index.ts index e24eaf4..78f63d6 100644 --- a/frontend/src/game/sprites/index.ts +++ b/frontend/src/game/sprites/index.ts @@ -201,14 +201,35 @@ export function generateSpriteSheet( if (tier >= 3) { px(cx + hOff, beltY, '#ffd700', ox, oy); px(cx + hOff + 1, beltY, '#ffd700', ox, oy) } } - // Chest emblem (tier 4+) - if (tier >= 4) { + // ₿ Bitcoin chest badge — every fighter carries the symbol + { const ey = by + Math.round(bh * 0.3) - px(cx + hOff - 1, ey, pal.acc, ox, oy) - px(cx + hOff, ey, pal.accLight, ox, oy) - px(cx + hOff + 1, ey, pal.acc, ox, oy) - px(cx + hOff, ey - 1, pal.acc, ox, oy) - px(cx + hOff, ey + 1, pal.acc, ox, oy) + const ec = cx + hOff + // Tier 0-1: simple orange ₿ dot + // Tier 2-3: small ₿ shape (vertical bar + bumps) + // Tier 4+: full golden ₿ with glow + if (tier >= 4) { + // Golden glow behind + px(ec - 2, ey, '#ffd70066', ox, oy) + px(ec + 2, ey, '#ffd70066', ox, oy) + px(ec, ey - 2, '#ffd70066', ox, oy) + px(ec, ey + 2, '#ffd70066', ox, oy) + } + const bc = tier >= 4 ? '#ffd700' : tier >= 2 ? '#ff8c00' : '#ff6600' + // Vertical stroke + px(ec, ey - 1, bc, ox, oy) + px(ec, ey, bc, ox, oy) + px(ec, ey + 1, bc, ox, oy) + // Right bumps (₿ shape) + if (bw >= 6) { + px(ec + 1, ey - 1, bc, ox, oy) + px(ec + 1, ey + 1, bc, ox, oy) + } + // Top/bottom serifs + if (tier >= 2) { + px(ec, ey - 2, tier >= 4 ? '#ffd700' : '#cc7000', ox, oy) + px(ec, ey + 2, tier >= 4 ? '#ffd700' : '#cc7000', ox, oy) + } } // Shoulder pads (tier 3+) diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index 57c774e..e9cfcb5 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -379,18 +379,26 @@ const tierClass = (t: number) => `tier-${t}`
-
+
+
+
+ {{ stats.satsWon }} +
-
+
`tier-${t}` :tier="stats.tier" :size="140" :customization="stats.customization || undefined" - class="drop-shadow-[0_0_20px_var(--glow)]" + class="drop-shadow-[0_0_20px_var(--glow)] relative z-10" :style="{ '--glow': stats.tierColor + '80' } as any" /> + +
+
+ {{ stats.satsWon }} +
diff --git a/frontend/src/pages/FightPage.vue b/frontend/src/pages/FightPage.vue index 498cf39..1845004 100644 --- a/frontend/src/pages/FightPage.vue +++ b/frontend/src/pages/FightPage.vue @@ -357,7 +357,12 @@ async function handleFightEnd(data: any) { if (liveScene) { liveScene.destroy(); liveScene = null } liveSceneReady.value = false - await loadFight() + // Retry loading until rounds are populated (server may still be persisting) + for (let attempt = 0; attempt < 5; attempt++) { + await loadFight() + if (fight.value?.rounds?.length) break + await new Promise(r => setTimeout(r, 800)) + } isLive.value = false disconnectSSE() stopHumanPolling() diff --git a/server/src/app.ts b/server/src/app.ts index 26511ff..5ebd77d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -36,15 +36,23 @@ app.use('*', logger()) const allowedOrigin = process.env.CORS_ORIGIN || '*' app.use('/api/*', cors({ origin: allowedOrigin })) +// COOP/COEP headers: required for SharedArrayBuffer (Kokoro TTS WASM threading) +app.use('*', async (c, next) => { + await next() + c.header('Cross-Origin-Opener-Policy', 'same-origin') + c.header('Cross-Origin-Embedder-Policy', 'credentialless') +}) + // 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'"], + scriptSrc: ["'self'", 'blob:'], styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], imgSrc: ["'self'", 'data:', 'blob:'], connectSrc: ["'self'"], fontSrc: ["'self'", 'https://fonts.gstatic.com'], + workerSrc: ["'self'", 'blob:'], } : undefined, })) diff --git a/server/src/engine/challenges-extra.ts b/server/src/engine/challenges-extra.ts index d6e3de6..03ad7f4 100644 --- a/server/src/engine/challenges-extra.ts +++ b/server/src/engine/challenges-extra.ts @@ -463,6 +463,14 @@ export const EXTRA_PROMPTS: Record = { { prompt: 'Roast someone who trusts custodial exchanges with their Bitcoin.' , theme: 'bitcoin' }, { prompt: 'Roast someone who calls themselves a "Web3 developer" but can\'t deploy a smart contract.' , theme: 'bot_coding' }, { prompt: 'Roast a sysadmin who uses root for everything.' , theme: 'bot_coding' }, + { prompt: 'Roast a "vibe coder" who writes entire apps with AI prompts and has no idea how any of it works. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast someone who replaced their entire engineering team with ChatGPT and wonders why production is on fire. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast a developer who says "just use AI" instead of learning to code. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast someone whose entire GitHub profile is AI-generated code they don\'t understand. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast a startup founder who pivoted to AI 6 times in one year. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast someone who uses 4 different AI coding assistants simultaneously and still can\'t ship. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast a developer who added Rust to a JavaScript project "for performance" and now nobody can build it. 3 sentences.', theme: 'bot_coding' }, + { prompt: 'Roast someone who says "I don\'t need tests, I have AI to find my bugs." 3 sentences.', theme: 'bot_coding' }, ], creative_writing: [ diff --git a/server/src/engine/dev-seed.ts b/server/src/engine/dev-seed.ts new file mode 100644 index 0000000..c068447 --- /dev/null +++ b/server/src/engine/dev-seed.ts @@ -0,0 +1,54 @@ +import { db, schema } from '../db/index.js' +import { sql } from 'drizzle-orm' +import { createTournament, joinTournament, startTournament } from './tournaments.js' +import { logger } from '../lib/logger.js' + +/** Seed a dev tournament with mock bots for testing betting/tournament UI */ +export async function seedDevTournament(): Promise { + // Skip if a tournament already exists + const existing = db.select({ count: sql`count(*)` }) + .from(schema.tournaments) + .get() + if (existing && existing.count > 0) { + logger.info('dev-seed', 'tournaments already exist — skipping seed') + return + } + + // Grab 8 mock bots for the tournament + const bots = db.select({ id: schema.bots.id, name: schema.bots.name }) + .from(schema.bots) + .limit(8) + .all() + + if (bots.length < 4) { + logger.warn('dev-seed', `only ${bots.length} bots — need at least 4 for tournament seed`) + return + } + + // Create tournament + const tournamentId = createTournament( + 'SATOSHI SHOWDOWN #1', + 'single_elim', + 8, + 100, + ) + + // Join bots + for (const bot of bots.slice(0, 8)) { + try { + joinTournament(tournamentId, bot.id) + } catch (err) { + logger.warn('dev-seed', `failed to join bot ${bot.name}: ${(err as Error).message}`) + } + } + + // Start the tournament (seeds by elo, generates bracket) + try { + startTournament(tournamentId) + logger.info('dev-seed', `seeded tournament "${tournamentId}" with ${Math.min(bots.length, 8)} bots`) + } catch (err) { + logger.warn('dev-seed', `failed to start tournament: ${(err as Error).message}`) + } + + logger.info('dev-seed', 'dev seeding complete — tournament ready for testing') +} diff --git a/server/src/index.ts b/server/src/index.ts index 3135c6c..74ea6e0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -6,6 +6,7 @@ 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' +import { seedDevTournament } from './engine/dev-seed.js' // Production env validation — warn but don't crash (wallet features degrade gracefully) if (process.env.NODE_ENV === 'production') { @@ -24,6 +25,11 @@ runMigrations() await seedMockBots() await seedClassicBots() +// Dev mode: seed tournament + betting data for testing +if (process.env.NODE_ENV !== 'production') { + await seedDevTournament() +} + const port = Number(process.env.PORT) || 9100 serve({ fetch: app.fetch, port }, () => {