feat: arena bot sprites, boxing-style Fight Card page
- Arena page shows small sprite previews next to each bot's name - Server returns archetype in fights list endpoint - New /fight-card page with 3D perspective fight poster, neon signs, animated boxing gloves, fighter bobbing, VS pulse, undercard list - Added FIGHT CARD to nav bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
95b53e271f
commit
ccf4196647
@@ -0,0 +1,309 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import SpritePreview from '../components/SpritePreview.vue'
|
||||
import PixelGlove from '../components/PixelGlove.vue'
|
||||
|
||||
interface UpcomingFight {
|
||||
id: string
|
||||
botA: { name: string; avatarSeed: string; archetype: string; tier: number; eloRating: number } | null
|
||||
botB: { name: string; avatarSeed: string; archetype: string; tier: number; eloRating: number } | null
|
||||
arenaInfo: { name: string } | null
|
||||
arena: string
|
||||
status: string
|
||||
totalRounds: number
|
||||
}
|
||||
|
||||
const fights = ref<UpcomingFight[]>([])
|
||||
const isLoading = ref(true)
|
||||
const featured = ref<UpcomingFight | null>(null)
|
||||
|
||||
const tierNames: Record<number, string> = {
|
||||
0: 'ROOKIE', 1: 'BRAWLER', 2: 'WARRIOR', 3: 'CHAMPION', 4: 'LEGEND', 5: 'MYTHIC',
|
||||
}
|
||||
const tierColors: Record<number, string> = {
|
||||
0: '#8a8a9a', 1: '#4ade80', 2: '#38bdf8', 3: '#a855f7', 4: '#f97316', 5: '#ef4444',
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/fights')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
fights.value = data
|
||||
// Feature the most recent fight with both bots
|
||||
featured.value = data.find((f: UpcomingFight) => f.botA && f.botB) || null
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[FightCard] load failed:', err)
|
||||
}
|
||||
isLoading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-4rem)] flex flex-col items-center overflow-y-auto relative">
|
||||
|
||||
<!-- Background neon grid -->
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[200%] h-full opacity-20"
|
||||
style="background: radial-gradient(ellipse at 50% 0%, rgba(255,45,120,0.4) 0%, transparent 60%)" />
|
||||
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-[200%] h-full opacity-15"
|
||||
style="background: radial-gradient(ellipse at 50% 100%, rgba(0,240,255,0.3) 0%, transparent 50%)" />
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
||||
<p class="font-display text-text-muted animate-pulse tracking-wider">LOADING...</p>
|
||||
</div>
|
||||
|
||||
<template v-else-if="featured">
|
||||
<!-- MAIN EVENT CARD -->
|
||||
<div class="w-full max-w-2xl mx-auto px-4 py-6 sm:py-10 relative z-10">
|
||||
|
||||
<!-- Neon sign header -->
|
||||
<div class="text-center mb-6 sm:mb-10">
|
||||
<p class="font-pixel text-[10px] sm:text-xs text-neon-yellow tracking-[0.4em] mb-2 animate-pulse">
|
||||
TONIGHT'S MAIN EVENT
|
||||
</p>
|
||||
<div class="relative inline-block">
|
||||
<h1 class="font-neon text-4xl sm:text-6xl md:text-7xl text-neon-pink glow-pink neon-flicker tracking-wider">
|
||||
FIGHT CARD
|
||||
</h1>
|
||||
<div class="absolute -inset-4 border-2 border-neon-pink/30 rounded-lg neon-border-pink" />
|
||||
<div class="absolute -inset-6 border border-neon-pink/10 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main fight card with 3D perspective -->
|
||||
<RouterLink
|
||||
:to="`/arena/${featured.id}`"
|
||||
class="block group"
|
||||
>
|
||||
<div class="fight-card-3d relative">
|
||||
<div class="fight-card-inner border-2 border-neon-pink/40 rounded-2xl overflow-hidden
|
||||
bg-gradient-to-b from-surface-raised via-black/90 to-surface-raised
|
||||
shadow-[0_0_40px_rgba(255,45,120,0.15),0_0_80px_rgba(255,45,120,0.05)]
|
||||
group-hover:border-neon-pink/60 group-hover:shadow-[0_0_60px_rgba(255,45,120,0.25)]
|
||||
transition-all duration-300">
|
||||
|
||||
<!-- Top banner -->
|
||||
<div class="bg-gradient-to-r from-neon-pink/20 via-neon-purple/20 to-neon-cyan/20 px-4 py-2 text-center border-b border-white/5">
|
||||
<p class="font-display font-black text-[10px] sm:text-xs tracking-[0.3em] text-neon-yellow">
|
||||
{{ featured.arenaInfo?.name?.toUpperCase() || 'THE RING' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Fighter showcase -->
|
||||
<div class="px-4 sm:px-8 py-6 sm:py-10">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
|
||||
<!-- Fighter A -->
|
||||
<div class="flex-1 flex flex-col items-center text-center min-w-0">
|
||||
<div class="relative mb-3 sm:mb-4">
|
||||
<div class="absolute -inset-3 rounded-full bg-neon-cyan/10 blur-xl" />
|
||||
<SpritePreview
|
||||
v-if="featured.botA"
|
||||
:seed="featured.botA.avatarSeed || featured.botA.name"
|
||||
:archetype="featured.botA.archetype || 'standard'"
|
||||
:tier="featured.botA.tier"
|
||||
:size="100"
|
||||
class="relative z-10 drop-shadow-[0_0_20px_rgba(0,240,255,0.4)] sm:w-[130px] sm:h-[130px] fighter-bob"
|
||||
/>
|
||||
</div>
|
||||
<p class="font-display font-black text-sm sm:text-xl tracking-wider text-neon-cyan glow-cyan truncate w-full">
|
||||
{{ featured.botA?.name || '???' }}
|
||||
</p>
|
||||
<p class="font-pixel text-[9px] sm:text-[10px] mt-1 tracking-wider"
|
||||
:style="{ color: tierColors[featured.botA?.tier || 0] }">
|
||||
{{ tierNames[featured.botA?.tier || 0] }}
|
||||
</p>
|
||||
<p class="font-mono text-[10px] sm:text-xs text-text-muted mt-0.5">
|
||||
{{ Math.round(featured.botA?.eloRating || 0) }} ELO
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- VS center with gloves -->
|
||||
<div class="shrink-0 flex flex-col items-center gap-1 px-2">
|
||||
<PixelGlove :size="28" class="sm:!w-[40px] rotate-12 glove-punch-left" />
|
||||
<div class="relative">
|
||||
<span class="font-neon text-3xl sm:text-5xl text-neon-purple glow-purple vs-pulse tracking-wider">VS</span>
|
||||
</div>
|
||||
<PixelGlove :size="28" flip class="sm:!w-[40px] -rotate-12 glove-punch-right" />
|
||||
</div>
|
||||
|
||||
<!-- Fighter B -->
|
||||
<div class="flex-1 flex flex-col items-center text-center min-w-0">
|
||||
<div class="relative mb-3 sm:mb-4">
|
||||
<div class="absolute -inset-3 rounded-full bg-neon-pink/10 blur-xl" />
|
||||
<SpritePreview
|
||||
v-if="featured.botB"
|
||||
:seed="featured.botB.avatarSeed || featured.botB.name"
|
||||
:archetype="featured.botB.archetype || 'standard'"
|
||||
:tier="featured.botB.tier"
|
||||
:size="100"
|
||||
class="relative z-10 drop-shadow-[0_0_20px_rgba(255,45,120,0.4)] sm:w-[130px] sm:h-[130px] fighter-bob-delayed"
|
||||
style="transform: scaleX(-1)"
|
||||
/>
|
||||
</div>
|
||||
<p class="font-display font-black text-sm sm:text-xl tracking-wider text-neon-pink glow-pink truncate w-full">
|
||||
{{ featured.botB?.name || '???' }}
|
||||
</p>
|
||||
<p class="font-pixel text-[9px] sm:text-[10px] mt-1 tracking-wider"
|
||||
:style="{ color: tierColors[featured.botB?.tier || 0] }">
|
||||
{{ tierNames[featured.botB?.tier || 0] }}
|
||||
</p>
|
||||
<p class="font-mono text-[10px] sm:text-xs text-text-muted mt-0.5">
|
||||
{{ Math.round(featured.botB?.eloRating || 0) }} ELO
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom banner -->
|
||||
<div class="bg-gradient-to-r from-neon-cyan/10 via-neon-purple/10 to-neon-pink/10 px-4 py-3 text-center border-t border-white/5">
|
||||
<p class="font-display font-black text-xs sm:text-sm tracking-[0.2em] text-neon-purple group-hover:text-neon-cyan transition-colors">
|
||||
{{ featured.status === 'finished' ? 'WATCH REPLAY' : featured.status === 'live' ? 'WATCH LIVE' : 'VIEW FIGHT' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<!-- Undercard fights -->
|
||||
<div v-if="fights.length > 1" class="mt-8 sm:mt-12">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-neon-purple/30 to-transparent" />
|
||||
<p class="font-pixel text-[10px] text-neon-purple tracking-[0.3em]">UNDERCARD</p>
|
||||
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-neon-purple/30 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<RouterLink
|
||||
v-for="fight in fights.filter(f => f.id !== featured?.id).slice(0, 6)"
|
||||
:key="fight.id"
|
||||
:to="`/arena/${fight.id}`"
|
||||
class="flex items-center px-3 sm:px-4 py-2.5 border border-border/50 rounded-lg
|
||||
bg-surface-raised/30 hover:border-neon-purple/30 hover:bg-surface-overlay/20
|
||||
transition-all group/card"
|
||||
>
|
||||
<div class="flex-1 flex items-center justify-end gap-2 min-w-0">
|
||||
<p class="font-display font-bold text-xs sm:text-sm truncate"
|
||||
:class="fight.botA ? 'text-text-primary' : 'text-text-muted'">
|
||||
{{ fight.botA?.name || '???' }}
|
||||
</p>
|
||||
<SpritePreview
|
||||
v-if="fight.botA"
|
||||
:seed="fight.botA.avatarSeed || fight.botA.name"
|
||||
:archetype="fight.botA.archetype || 'standard'"
|
||||
:tier="fight.botA.tier"
|
||||
:size="28"
|
||||
class="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
<span class="font-funky text-neon-purple text-xs sm:text-sm px-2 sm:px-3 shrink-0">VS</span>
|
||||
<div class="flex-1 flex items-center gap-2 min-w-0">
|
||||
<SpritePreview
|
||||
v-if="fight.botB"
|
||||
:seed="fight.botB.avatarSeed || fight.botB.name"
|
||||
:archetype="fight.botB.archetype || 'standard'"
|
||||
:tier="fight.botB.tier"
|
||||
:size="28"
|
||||
class="shrink-0"
|
||||
style="transform: scaleX(-1)"
|
||||
/>
|
||||
<p class="font-display font-bold text-xs sm:text-sm truncate"
|
||||
:class="fight.botB ? 'text-text-primary' : 'text-text-muted'">
|
||||
{{ fight.botB?.name || '???' }}
|
||||
</p>
|
||||
</div>
|
||||
<span v-if="fight.status === 'finished'" class="font-mono text-[9px] text-text-muted ml-2 shrink-0">R{{ fight.totalRounds }}</span>
|
||||
<span v-else-if="fight.status === 'live'" class="font-display text-[10px] text-neon-yellow ml-2 shrink-0 animate-pulse">LIVE</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="mt-8 sm:mt-10 text-center">
|
||||
<RouterLink
|
||||
to="/join"
|
||||
class="inline-block px-8 sm:px-12 py-3 sm:py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
||||
font-display font-black text-sm sm:text-base tracking-widest
|
||||
hover:bg-neon-pink/20 transition-all neon-border-pink"
|
||||
>
|
||||
ENTER THE RING
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="flex-1 flex flex-col items-center justify-center gap-4 relative z-10">
|
||||
<p class="font-display text-text-muted text-lg tracking-wider">No fights scheduled.</p>
|
||||
<RouterLink
|
||||
to="/join"
|
||||
class="px-8 py-3 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
||||
font-display font-black text-sm tracking-widest hover:bg-neon-pink/20 transition-all"
|
||||
>
|
||||
START A FIGHT
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 3D card effect */
|
||||
.fight-card-3d {
|
||||
perspective: 1000px;
|
||||
}
|
||||
.fight-card-inner {
|
||||
transform: rotateX(2deg);
|
||||
transform-origin: center bottom;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.fight-card-3d:hover .fight-card-inner {
|
||||
transform: rotateX(0deg) scale(1.01);
|
||||
}
|
||||
|
||||
/* Fighter bobbing */
|
||||
.fighter-bob {
|
||||
animation: fighterBob 2.5s ease-in-out infinite;
|
||||
}
|
||||
.fighter-bob-delayed {
|
||||
animation: fighterBob 2.5s ease-in-out infinite 0.5s;
|
||||
}
|
||||
@keyframes fighterBob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* Glove punch animations */
|
||||
.glove-punch-left {
|
||||
animation: punchLeft 2s ease-in-out infinite;
|
||||
}
|
||||
.glove-punch-right {
|
||||
animation: punchRight 2s ease-in-out infinite 0.3s;
|
||||
}
|
||||
@keyframes punchLeft {
|
||||
0%, 70%, 100% { transform: rotate(12deg) translateX(0); }
|
||||
80% { transform: rotate(25deg) translateX(6px); }
|
||||
85% { transform: rotate(8deg) translateX(-2px); }
|
||||
}
|
||||
@keyframes punchRight {
|
||||
0%, 70%, 100% { transform: rotate(-12deg) translateX(0); }
|
||||
80% { transform: rotate(-25deg) translateX(-6px); }
|
||||
85% { transform: rotate(-8deg) translateX(2px); }
|
||||
}
|
||||
|
||||
/* VS pulse */
|
||||
.vs-pulse {
|
||||
animation: vsPulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes vsPulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
}
|
||||
|
||||
/* Neon purple glow utility */
|
||||
.glow-purple {
|
||||
text-shadow: 0 0 10px rgba(168, 85, 247, 0.6), 0 0 30px rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user