fix: fight restart bug, mobile TTS, character sizing, bitcoin symbols

- 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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 08:21:16 +00:00
co-authored by Claude Opus 4.6
parent a10c1c9cb6
commit 5ee6b6c88c
10 changed files with 141 additions and 16 deletions
+9 -1
View File
@@ -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,
}))
+8
View File
@@ -463,6 +463,14 @@ export const EXTRA_PROMPTS: Record<string, PromptEntry[]> = {
{ 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: [
+54
View File
@@ -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<void> {
// Skip if a tournament already exists
const existing = db.select({ count: sql<number>`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')
}
+6
View File
@@ -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 }, () => {