feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth

- Add queue-based matchmaking with Elo-proximity and 10s timeout
- Procedural sound engine (SFX, voice announcer, 4-track music)
- Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank)
- 42+ fight choreographies with themed/generic/wild card selection
- 4 KO finish styles, super-speed mode, hyperdetail close-ups
- Auth routes, JoinBout page, bot profile with stats
- 7-tier ranking system (Baby through Legend)
- Arena and challenge system expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 22:13:19 +00:00
co-authored by Claude Opus 4.6
parent 335c148866
commit 47d20fbe66
82 changed files with 14011 additions and 741 deletions
+184
View File
@@ -0,0 +1,184 @@
import { ref, readonly, computed } from 'vue'
interface BotData {
id: string
name: string
avatarSeed: string
archetype: string
profilePicUrl: string | null
eloRating: number
wins: number
losses: number
winStreak: number
bestStreak: number
tier: number
}
interface NostrWindow {
getPublicKey(): Promise<string>
signEvent(event: Record<string, unknown>): Promise<Record<string, unknown>>
getRelays?(): Promise<Record<string, { read: boolean; write: boolean }>>
}
declare global {
interface Window {
nostr?: NostrWindow
}
}
const pubkey = ref<string | null>(null)
const bot = ref<BotData | null>(null)
const profilePicUrl = ref<string | null>(null)
const isLoading = ref(false)
export function useNostr() {
const isLoggedIn = computed(() => !!pubkey.value && !!bot.value)
const hasExtension = computed(() => !!window.nostr)
async function login(): Promise<{ pubkey: string; bot: BotData | null }> {
if (!window.nostr) {
throw new Error('No Nostr extension found. Install nos2x, Alby, or another NIP-07 extension.')
}
isLoading.value = true
try {
const pk = await window.nostr.getPublicKey()
pubkey.value = pk
// Fetch Nostr profile pic from relay
const pic = await fetchNostrProfilePic(pk)
if (pic) profilePicUrl.value = pic
// Check if this pubkey has a bot
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk }),
})
if (res.ok) {
const data = await res.json()
if (data.exists) {
bot.value = data.bot
return { pubkey: pk, bot: data.bot }
}
}
return { pubkey: pk, bot: null }
} finally {
isLoading.value = false
}
}
async function registerBot(name: string, webhookUrl: string, archetype: string): Promise<BotData> {
if (!pubkey.value) throw new Error('Not logged in')
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pubkey: pubkey.value,
name,
webhookUrl,
archetype,
profilePicUrl: profilePicUrl.value,
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Registration failed')
bot.value = {
id: data.id,
name: data.name,
avatarSeed: data.name,
archetype: data.archetype,
profilePicUrl: profilePicUrl.value,
eloRating: 1200,
wins: 0,
losses: 0,
winStreak: 0,
bestStreak: 0,
tier: 0,
}
return bot.value
}
function logout() {
pubkey.value = null
bot.value = null
profilePicUrl.value = null
}
return {
pubkey: readonly(pubkey),
bot: readonly(bot),
profilePicUrl: readonly(profilePicUrl),
isLoggedIn,
isLoading: readonly(isLoading),
hasExtension,
login,
registerBot,
logout,
}
}
// Fetch profile picture from a Nostr relay
async function fetchNostrProfilePic(pk: string): Promise<string | null> {
const relays = [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol',
]
for (const relay of relays) {
try {
const pic = await queryRelay(relay, pk)
if (pic) return pic
} catch {
continue
}
}
return null
}
function queryRelay(url: string, pk: string): Promise<string | null> {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
ws.close()
resolve(null)
}, 3000)
const ws = new WebSocket(url)
const subId = Math.random().toString(36).slice(2, 10)
ws.onopen = () => {
// Request kind 0 (metadata) for this pubkey
ws.send(JSON.stringify(['REQ', subId, { kinds: [0], authors: [pk], limit: 1 }]))
}
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data)
if (data[0] === 'EVENT' && data[2]?.kind === 0) {
const meta = JSON.parse(data[2].content)
clearTimeout(timeout)
ws.close()
resolve(meta.picture || null)
} else if (data[0] === 'EOSE') {
clearTimeout(timeout)
ws.close()
resolve(null)
}
} catch {
// ignore parse errors
}
}
ws.onerror = () => {
clearTimeout(timeout)
resolve(null)
}
})
}