feat: v5 — boxing poster fight cards, 12-char names, diverse mock bots

- Fight Card page: dramatic poster background with cross-hatch, spotlights,
  vignettes, corner brackets, scan lines; 3D VS orb with punch animation;
  selectable undercard with main event always pinned at top
- PosterSprite: high-quality 480px poster frame with 6-pass renderer
  (aura, glow, bevel, specular, particles); PixelGlove component
- 12-char bot name limit across all forms and server validation
- Mock bots: all 100 now have diverse archetypes (25 types), 25% human
  fighters; seedMockBots updates existing bots on restart
- Leaderboard: inline SpritePreview next to each bot name
- Nostr auth: persistent login, nsec copy button
- Wallet: NWC + Lightning Address, ranked fight flow
- Server: payments, ranked queue, customization endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 10:33:30 +00:00
co-authored by Claude Opus 4.6
parent ccf4196647
commit f6eb7d2845
32 changed files with 1743 additions and 467 deletions
+42 -19
View File
@@ -1,8 +1,9 @@
import { ref, readonly, computed } from 'vue'
import { useNostr } from './useNostr.js'
import { finalizeEvent } from 'nostr-tools'
import * as nip04 from 'nostr-tools/nip04'
import * as nip44 from 'nostr-tools/nip44'
import { hexToBytes } from 'nostr-tools/utils'
import { hexToBytes, bytesToHex } from 'nostr-tools/utils'
type WalletMethod = 'nwc' | 'lnaddress' | null
type PaymentStatus = 'idle' | 'invoiced' | 'paying' | 'confirmed' | 'failed'
@@ -150,14 +151,29 @@ export function useWallet() {
const { bolt11, paymentId } = await invoiceRes.json()
pendingPayment.value = { paymentId, bolt11 }
// If NWC connected, auto-pay via NWC
// Dev mode: server auto-confirmed, skip payment
if (bolt11 === 'dev_auto_confirmed') {
paymentStatus.value = 'confirmed'
return paymentId
}
// If NWC connected, auto-pay via NWC and confirm directly
const nwcUrl = localStorage.getItem('bf_nwc_url')
if (nwcUrl) {
paymentStatus.value = 'paying'
await payViaNWC(nwcUrl, bolt11)
const preimage = await payViaNWC(nwcUrl, bolt11)
// Tell server payment is confirmed (skip lookup_invoice polling)
await fetch(`/api/payments/confirm/${paymentId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preimage, pubkey: pubkey.value }),
})
paymentStatus.value = 'confirmed'
return paymentId
}
// Poll for confirmation
// No NWC — poll for confirmation (manual payment / QR code flow)
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 2000))
const checkRes = await fetch(`/api/payments/check/${paymentId}`)
@@ -220,13 +236,13 @@ export function useWallet() {
}
/** Send a pay_invoice request via NWC WebSocket */
async function payViaNWC(nwcUrl: string, bolt11: string): Promise<void> {
async function payViaNWC(nwcUrl: string, bolt11: string): Promise<string | undefined> {
const nwc = parseNwcUrl(nwcUrl)
const conversationKey = nip44.v2.utils.getConversationKey(nwc.secret, nwc.pubkey)
const secretHex = bytesToHex(nwc.secret)
const content = nip44.v2.encrypt(
// Use NIP-04 encryption (BTCPay/LND compatibility)
const content = await nip04.encrypt(secretHex, nwc.pubkey,
JSON.stringify({ method: 'pay_invoice', params: { invoice: bolt11 } }),
conversationKey,
)
const event = finalizeEvent({
@@ -240,41 +256,48 @@ async function payViaNWC(nwcUrl: string, bolt11: string): Promise<void> {
const ws = new WebSocket(nwc.relay)
const timeout = setTimeout(() => {
ws.close()
// Don't reject — the server-side poll will catch confirmation
resolve()
resolve(undefined)
}, 30_000)
ws.onopen = () => {
// Subscribe for response
const subId = Math.random().toString(36).slice(2, 10)
ws.send(JSON.stringify(['REQ', subId, {
kinds: [23195],
authors: [nwc.pubkey],
'#e': [event.id],
}]))
// Publish the payment request
ws.send(JSON.stringify(['EVENT', event]))
}
ws.onmessage = (msg) => {
ws.onmessage = async (msg) => {
try {
const data = JSON.parse(msg.data)
if (data[0] === 'EVENT' && data[2]?.kind === 23195) {
clearTimeout(timeout)
ws.close()
// Payment sent — server-side poll will confirm
resolve()
// Decrypt response — try NIP-04 first, fall back to NIP-44
let decrypted: string
try {
decrypted = await nip04.decrypt(secretHex, nwc.pubkey, data[2].content)
} catch {
const convKey = nip44.v2.utils.getConversationKey(nwc.secret, nwc.pubkey)
decrypted = nip44.v2.decrypt(data[2].content, convKey)
}
const result = JSON.parse(decrypted)
if (result.error) {
reject(new Error(result.error.message || 'NWC payment failed'))
} else {
resolve(result.result?.preimage)
}
}
} catch {
// ignore
// ignore parse errors
}
}
ws.onerror = () => {
clearTimeout(timeout)
// Don't reject — server-side poll is the source of truth
resolve()
resolve(undefined)
}
})
}