feat: practice button on profile, fix rate limiter, fix mobile sprite rendering

- Add Practice button to BotProfilePage for quick sparring
- Fix rate limiter bug: all rateLimit() instances shared one counter map,
  causing global and per-route limits to corrupt each other. Each limiter
  now gets its own isolated map.
- Replace 8-digit hex colors (#ffd70066) with rgba() in sprite rendering
  for mobile browser compatibility (iOS Safari renders them as black boxes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 12:14:07 +00:00
co-authored by Claude Opus 4.6
parent d0f0a84a57
commit ad96d1158f
4 changed files with 77 additions and 21 deletions
+41
View File
@@ -7,6 +7,7 @@ import HumanPreview from '../components/HumanPreview.vue'
import WalletConnect from '../components/WalletConnect.vue'
import BetHistory from '../components/BetHistory.vue'
import type { SpriteCustomization } from '../game/sprites'
import { ensureAudioContext } from '../game/audio'
const route = useRoute()
const router = useRouter()
@@ -84,6 +85,7 @@ interface QueueEntry {
const stats = ref<BotStats | null>(null)
const isLoading = ref(true)
const isJoining = ref(false)
const isJoiningPractice = ref(false)
const showChoose = ref(false)
const waitingFighters = ref<QueueEntry[]>([])
let pollHandle: ReturnType<typeof setInterval> | null = null
@@ -345,6 +347,31 @@ async function fightSpecific(opponentBotId: string) {
isJoining.value = false
}
async function practice() {
if (!stats.value || isJoiningPractice.value) return
isJoiningPractice.value = true
fightError.value = ''
ensureAudioContext()
try {
const res = await fetch(`/api/fights/practice/${stats.value.id}`, { method: 'POST' })
if (res.ok) {
const data = await res.json()
router.push(`/arena/${data.fightId}`)
return
} else {
const data = await res.json()
if (data.fightId) {
fightError.value = 'Already in a fight!'
} else {
fightError.value = data.retryAfterSec ? `${data.error} (${data.retryAfterSec}s)` : (data.error || 'Practice failed.')
}
}
} catch {
fightError.value = 'Network error.'
}
isJoiningPractice.value = false
}
function handleSignOut() {
logout()
router.push('/')
@@ -548,6 +575,20 @@ const tierClass = (t: number) => `tier-${t}`
</button>
</div>
<!-- Practice -->
<button
class="w-full mt-2 py-2.5 bg-white/3 border border-white/10 text-text-muted
font-display font-bold text-xs tracking-widest
hover:bg-white/5 hover:text-text-secondary transition-all
disabled:opacity-50 disabled:cursor-wait
flex items-center justify-center gap-2"
:disabled="isJoiningPractice"
@click="practice"
>
<span v-if="isJoiningPractice" class="w-3.5 h-3.5 border-2 border-white/20 border-t-white/50 rounded-full animate-spin" />
{{ isJoiningPractice ? 'STARTING...' : 'PRACTICE' }}
</button>
<p v-if="fightError" class="font-mono text-[10px] text-neon-pink mt-2">{{ fightError }}</p>
<!-- Choose your fight panel -->