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
+20 -4
View File
@@ -26,8 +26,8 @@ interface Round {
interface FightData {
id: string
botA: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record<string, unknown> | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number } | null
botB: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record<string, unknown> | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number } | null
botA: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record<string, unknown> | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number; botType?: string } | null
botB: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record<string, unknown> | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number; botType?: string } | null
arenaInfo: { id: string; name: string; description: string; modifier: string | null } | null
arena: string
winnerId: string | null
@@ -47,6 +47,7 @@ const logEl = ref<HTMLElement>()
let scene: FightSceneController | null = null
const sceneReady = ref(false)
let cleanupTimerHandle: ReturnType<typeof setTimeout> | null = null
let destroyed = false
const isReplaying = ref(false)
const displayHpA = ref(100)
@@ -96,6 +97,7 @@ function mapHp(hp: number, winnerId: string | null, botId: string | undefined):
}
onUnmounted(() => {
destroyed = true
stopAllAudio()
sceneReady.value = false
if (scene) { scene.destroy(); scene = null }
@@ -157,7 +159,14 @@ const challengeLabel = (type: string) => {
}
const tierClass = (t: number) => `tier-${t}`
function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) }
function sleep(ms: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (destroyed) reject(new Error('unmounted'))
else resolve()
}, ms)
})
}
function scrollLog() { nextTick(() => { logEl.value?.scrollTo({ top: logEl.value.scrollHeight, behavior: 'smooth' }) }) }
// Show floating announcement over the canvas
@@ -220,6 +229,14 @@ async function replay() {
if (isReplaying.value || !props.fight.botA || !props.fight.botB) return
isReplaying.value = true
showingFinal.value = false
try { await _doReplay() } catch (e) {
if (e instanceof Error && e.message === 'unmounted') return
console.error('[FightViewer] replay error:', e)
} finally { isReplaying.value = false }
}
async function _doReplay() {
if (!props.fight.botA || !props.fight.botB) return
displayHpA.value = 100
displayHpB.value = 100
logItems.value = []
@@ -412,7 +429,6 @@ async function replay() {
cleanupTimerHandle = null
}, 3000)
isReplaying.value = false
emit('replay-done')
}
</script>