feat: comedy overhaul start, profile page, bot setup docs, auth improvements

Rewrites announcer commentary (HYPE_LINES, DEEP_INTROS, ROUND_HYPE) with
modern edgy humor. Adds BOT_SETUP.md, bot SDK, customization engine,
profile page character display, persistent auth, rate limit tweaks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 08:52:24 +00:00
co-authored by Claude Opus 4.6
parent 4a35e1e0b5
commit 559782c8ce
15 changed files with 1086 additions and 58 deletions
+138 -2
View File
@@ -1,19 +1,30 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter, RouterLink } from 'vue-router'
import { useNostr } from '../composables/useNostr'
import SpritePreview from '../components/SpritePreview.vue'
import type { SpriteCustomization } from '../game/sprites'
const route = useRoute()
const router = useRouter()
const { bot: nostrBot, isLoggedIn, logout } = useNostr()
const { bot: nostrBot, isLoggedIn, logout, updateCustomization } = useNostr()
const botName = route.params.name as string
interface BotCustomization {
archetype?: string
primaryColor?: string
secondaryColor?: string
forceVisor?: boolean
forceMohawk?: boolean
forceHorns?: boolean
}
interface BotStats {
id: string
name: string
avatarSeed: string
archetype: string
customization: BotCustomization | null
profilePicUrl: string | null
eloRating: number
wins: number
@@ -65,6 +76,131 @@ const waitingFighters = ref<QueueEntry[]>([])
let pollHandle: ReturnType<typeof setInterval> | null = null
const isOwner = ref(false)
const showCustomize = ref(false)
const isSaving = ref(false)
const custError = ref('')
const ARCHETYPES = [
'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat',
'cactus', 'pizza', 'mushroom', 'shark', 'penguin', 'octopus', 'skeleton',
'ghost', 'alien', 'dinosaur', 'pirate', 'ninja', 'cowboy', 'wizard',
'bee', 'frog', 'snail', 'robot', 'android', 'drone', 'toaster', 'tv_head',
'calculator', 'satellite', 'mech', 'led_cube', 'circuit', 'antenna_bot',
'microwave', 'cyberdog', 'robocat', 'ufo_bot', 'minotaur', 'unicorn',
'phoenix', 'dragon', 'mermaid', 'griffin', 'cyclops', 'gargoyle', 'golem',
'vampire', 'werewolf', 'zombie', 'witch', 'demon', 'chef', 'firefighter',
'astronaut', 'clown', 'detective', 'nurse', 'lumberjack', 'scientist',
'wrestler', 'boxer', 'gladiator', 'samurai', 'viking', 'knight',
'elephant', 'giraffe', 'hippo', 'lion', 'monkey', 'parrot', 'raccoon',
'snake', 'turtle', 'whale', 'crocodile', 'flamingo', 'hedgehog', 'panda',
'hamster', 'sock_puppet', 'traffic_cone', 'toilet_man', 'potato',
'cloud_man', 'rock_man', 'balloon_man', 'trash_can', 'rubber_duck',
'snowman', 'scarecrow', 'jack_o_lantern', 'garden_gnome', 'lamp_post',
'broom_man',
]
const custForm = reactive({
archetype: '',
primaryColor: '#3388cc',
secondaryColor: '#cc8833',
forceVisor: false,
forceMohawk: false,
forceHorns: false,
})
function hslToHex(hsl: string): string {
const m = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
if (!m) return '#888888'
const h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1; if (t > 1) t -= 1
if (t < 1/6) return p + (q - p) * 6 * t
if (t < 1/2) return q
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
return p
}
let r: number, g: number, b: number
if (s === 0) { r = g = b = l }
else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
}
const hex = (v: number) => Math.round(v * 255).toString(16).padStart(2, '0')
return `#${hex(r)}${hex(g)}${hex(b)}`
}
function hexToHsl(hex: string): string {
const r = parseInt(hex.slice(1, 3), 16) / 255
const g = parseInt(hex.slice(3, 5), 16) / 255
const b = parseInt(hex.slice(5, 7), 16) / 255
const max = Math.max(r, g, b), min = Math.min(r, g, b)
const l = (max + min) / 2
if (max === min) return `hsl(0, 0%, ${Math.round(l * 100)}%)`
const d = max - min
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
let h = 0
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6
else if (max === g) h = ((b - r) / d + 2) / 6
else h = ((r - g) / d + 4) / 6
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`
}
const previewCustomization = computed<SpriteCustomization>(() => ({
archetype: custForm.archetype || undefined,
primaryColor: hexToHsl(custForm.primaryColor),
secondaryColor: hexToHsl(custForm.secondaryColor),
forceVisor: custForm.forceVisor,
forceMohawk: custForm.forceMohawk,
forceHorns: custForm.forceHorns,
}))
function initCustForm() {
if (!stats.value) return
const c = stats.value.customization
custForm.archetype = c?.archetype || stats.value.archetype || ''
custForm.primaryColor = c?.primaryColor ? hslToHex(c.primaryColor) : '#3388cc'
custForm.secondaryColor = c?.secondaryColor ? hslToHex(c.secondaryColor) : '#cc8833'
custForm.forceVisor = c?.forceVisor ?? false
custForm.forceMohawk = c?.forceMohawk ?? false
custForm.forceHorns = c?.forceHorns ?? false
}
async function saveCustomization() {
if (isSaving.value) return
isSaving.value = true
custError.value = ''
try {
await updateCustomization({
archetype: custForm.archetype || undefined,
primaryColor: hexToHsl(custForm.primaryColor),
secondaryColor: hexToHsl(custForm.secondaryColor),
forceVisor: custForm.forceVisor,
forceMohawk: custForm.forceMohawk,
forceHorns: custForm.forceHorns,
})
if (stats.value) {
stats.value = {
...stats.value,
archetype: custForm.archetype || stats.value.archetype,
customization: {
archetype: custForm.archetype || undefined,
primaryColor: hexToHsl(custForm.primaryColor),
secondaryColor: hexToHsl(custForm.secondaryColor),
forceVisor: custForm.forceVisor,
forceMohawk: custForm.forceMohawk,
forceHorns: custForm.forceHorns,
},
}
}
showCustomize.value = false
} catch (err) {
custError.value = err instanceof Error ? err.message : 'Save failed'
}
isSaving.value = false
}
onMounted(async () => {
try {