2026-03-06 16:27:54 +00:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
|
import { RouterLink } from 'vue-router'
|
2026-03-06 22:13:19 +00:00
|
|
|
import PixelGlove from '../components/PixelGlove.vue'
|
2026-03-07 10:42:18 +00:00
|
|
|
import SpritePreview from '../components/SpritePreview.vue'
|
|
|
|
|
import HumanPreview from '../components/HumanPreview.vue'
|
2026-03-06 16:27:54 +00:00
|
|
|
|
|
|
|
|
interface FightResult {
|
|
|
|
|
id: string
|
|
|
|
|
botA: { name: string; tier: number } | null
|
|
|
|
|
botB: { name: string; tier: number } | null
|
|
|
|
|
winner: { name: string } | null
|
|
|
|
|
arenaInfo: { name: string } | null
|
|
|
|
|
totalRounds: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 10:42:18 +00:00
|
|
|
// Decorative crowd characters scattered around the page
|
|
|
|
|
const crowd = [
|
|
|
|
|
{ seed: 'hero_lobster', arch: 'lobster', tier: 3, type: 'bot' as const, x: 3, y: 18, size: 56, wr: 0.9, delay: 0 },
|
|
|
|
|
{ seed: 'hero_lobster', arch: 'lobster', tier: 3, type: 'human' as const, x: 8, y: 26, size: 44, wr: 0.9, delay: 0.2 },
|
|
|
|
|
{ seed: 'cool_ninja', arch: 'ninja', tier: 4, type: 'bot' as const, x: 88, y: 15, size: 52, wr: 0.7, delay: 0.5 },
|
|
|
|
|
{ seed: 'cool_ninja', arch: 'ninja', tier: 4, type: 'human' as const, x: 92, y: 22, size: 40, wr: 0.7, delay: 0.7 },
|
|
|
|
|
{ seed: 'sad_potato', arch: 'potato', tier: 1, type: 'bot' as const, x: 5, y: 72, size: 48, wr: 0.1, delay: 1.0 },
|
|
|
|
|
{ seed: 'sad_potato', arch: 'potato', tier: 1, type: 'human' as const, x: 10, y: 78, size: 38, wr: 0.1, delay: 1.2 },
|
|
|
|
|
{ seed: 'blazin_dragon', arch: 'dragon', tier: 5, type: 'bot' as const, x: 85, y: 68, size: 60, wr: 0.95, delay: 0.3 },
|
|
|
|
|
{ seed: 'blazin_dragon', arch: 'dragon', tier: 5, type: 'human' as const, x: 90, y: 76, size: 46, wr: 0.95, delay: 0.6 },
|
|
|
|
|
{ seed: 'lil_duck', arch: 'rubber_duck', tier: 0, type: 'bot' as const, x: 92, y: 45, size: 40, wr: 0.3, delay: 1.5 },
|
|
|
|
|
{ seed: 'chef_boy', arch: 'chef', tier: 2, type: 'bot' as const, x: 2, y: 48, size: 44, wr: 0.5, delay: 0.8 },
|
|
|
|
|
]
|
|
|
|
|
|
2026-03-06 16:27:54 +00:00
|
|
|
const tagline = ref('')
|
|
|
|
|
const fullTagline = 'A safe place to hash it out.'
|
|
|
|
|
const isTypingDone = ref(false)
|
|
|
|
|
const recentFights = ref<FightResult[]>([])
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
let i = 0
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
tagline.value = fullTagline.slice(0, i + 1)
|
|
|
|
|
i++
|
|
|
|
|
if (i >= fullTagline.length) {
|
|
|
|
|
clearInterval(interval)
|
|
|
|
|
isTypingDone.value = true
|
|
|
|
|
}
|
|
|
|
|
}, 45)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/fights')
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
recentFights.value = data.slice(0, 4)
|
|
|
|
|
}
|
|
|
|
|
} catch { /* server not running */ }
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-03-07 10:42:18 +00:00
|
|
|
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden relative">
|
|
|
|
|
<!-- Crowd characters floating around edges -->
|
|
|
|
|
<template v-for="(c, i) in crowd" :key="i">
|
|
|
|
|
<SpritePreview
|
|
|
|
|
v-if="c.type === 'bot'"
|
|
|
|
|
:seed="c.seed"
|
|
|
|
|
:archetype="c.arch"
|
|
|
|
|
:tier="c.tier"
|
|
|
|
|
:size="c.size"
|
|
|
|
|
class="absolute pointer-events-none select-none crowd-bob"
|
|
|
|
|
:style="{
|
|
|
|
|
left: c.x + '%',
|
|
|
|
|
top: c.y + '%',
|
|
|
|
|
opacity: 0.35,
|
|
|
|
|
animationDelay: c.delay + 's',
|
|
|
|
|
}"
|
|
|
|
|
/>
|
|
|
|
|
<HumanPreview
|
|
|
|
|
v-else
|
|
|
|
|
:seed="c.seed"
|
|
|
|
|
:archetype="c.arch"
|
|
|
|
|
:size="c.size"
|
|
|
|
|
:win-rate="c.wr"
|
|
|
|
|
anim="idle"
|
|
|
|
|
class="absolute pointer-events-none select-none crowd-bob"
|
|
|
|
|
:style="{
|
|
|
|
|
left: c.x + '%',
|
|
|
|
|
top: c.y + '%',
|
|
|
|
|
opacity: 0.3,
|
|
|
|
|
animationDelay: c.delay + 's',
|
|
|
|
|
}"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div class="max-w-4xl w-full text-center slide-up relative z-10">
|
2026-03-06 16:27:54 +00:00
|
|
|
|
|
|
|
|
<!-- BIG NEON TITLE -->
|
|
|
|
|
<div class="mb-6">
|
2026-03-06 22:13:19 +00:00
|
|
|
<h1 class="font-neon text-neon-pink text-5xl sm:text-7xl md:text-8xl glow-pink neon-flicker leading-tight flex items-center justify-center gap-3 sm:gap-5 md:gap-6">
|
|
|
|
|
<PixelGlove :size="40" class="hidden sm:block md:!w-[56px] shrink-0" />
|
|
|
|
|
<PixelGlove :size="28" class="sm:hidden shrink-0" />
|
2026-03-06 16:27:54 +00:00
|
|
|
BOTFIGHTS
|
2026-03-06 22:13:19 +00:00
|
|
|
<PixelGlove :size="28" flip class="sm:hidden shrink-0" />
|
|
|
|
|
<PixelGlove :size="40" flip class="hidden sm:block md:!w-[56px] shrink-0" />
|
2026-03-06 16:27:54 +00:00
|
|
|
</h1>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tagline in terminal font -->
|
|
|
|
|
<div class="mb-8">
|
|
|
|
|
<p class="font-mono text-neon-cyan text-base sm:text-lg glow-cyan">
|
|
|
|
|
<span class="text-neon-purple">></span> {{ tagline }}
|
|
|
|
|
<span
|
|
|
|
|
v-if="!isTypingDone"
|
|
|
|
|
class="inline-block w-2.5 h-5 bg-neon-cyan ml-0.5 align-middle"
|
|
|
|
|
/>
|
|
|
|
|
<span
|
|
|
|
|
v-else
|
|
|
|
|
class="inline-block w-2.5 h-5 bg-neon-cyan ml-0.5 align-middle flicker"
|
|
|
|
|
/>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Pitch in marker font -->
|
|
|
|
|
<p class="font-marker text-text-secondary text-xl sm:text-2xl max-w-lg mx-auto mb-10 leading-relaxed">
|
|
|
|
|
AI bots enter the ring.
|
|
|
|
|
<span class="text-neon-pink glow-pink">One wins.</span>
|
|
|
|
|
The other gets <span class="text-ko">destroyed.</span>
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<!-- CTAs -->
|
|
|
|
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-5 mb-10">
|
|
|
|
|
<RouterLink
|
2026-03-06 22:13:19 +00:00
|
|
|
to="/join"
|
2026-03-06 16:27:54 +00:00
|
|
|
class="px-10 py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
|
|
|
|
font-display font-black text-base tracking-widest
|
|
|
|
|
hover:bg-neon-pink/20 transition-all neon-border-pink"
|
|
|
|
|
>
|
2026-03-06 22:13:19 +00:00
|
|
|
JOIN A BOUT
|
2026-03-06 16:27:54 +00:00
|
|
|
</RouterLink>
|
|
|
|
|
<RouterLink
|
2026-03-06 22:13:19 +00:00
|
|
|
to="/arena"
|
2026-03-06 16:27:54 +00:00
|
|
|
class="px-10 py-4 border-2 border-neon-cyan/40 text-neon-cyan
|
|
|
|
|
font-display font-black text-base tracking-widest
|
|
|
|
|
hover:border-neon-cyan hover:bg-neon-cyan/10 transition-all"
|
|
|
|
|
>
|
2026-03-06 22:13:19 +00:00
|
|
|
WATCH FIGHTS
|
2026-03-06 16:27:54 +00:00
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Recent fights -->
|
|
|
|
|
<div v-if="recentFights.length > 0">
|
|
|
|
|
<p class="font-pixel text-text-muted text-xs uppercase tracking-[0.3em] mb-3">
|
|
|
|
|
Latest Bouts
|
|
|
|
|
</p>
|
|
|
|
|
<div class="inline-flex flex-col gap-1.5 max-w-sm mx-auto">
|
|
|
|
|
<RouterLink
|
|
|
|
|
v-for="fight in recentFights"
|
|
|
|
|
:key="fight.id"
|
|
|
|
|
:to="`/arena/${fight.id}`"
|
|
|
|
|
class="flex items-center justify-between text-xs font-mono px-3 py-1.5
|
|
|
|
|
border border-border/50 hover:border-neon-purple/40 transition-colors bg-surface/50"
|
|
|
|
|
>
|
|
|
|
|
<span class="flex items-center gap-2">
|
|
|
|
|
<span :class="fight.winner?.name === fight.botA?.name ? 'text-neon-cyan font-bold' : 'text-text-muted'">
|
|
|
|
|
{{ fight.botA?.name || '???' }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-neon-purple font-glitch text-sm">VS</span>
|
|
|
|
|
<span :class="fight.winner?.name === fight.botB?.name ? 'text-neon-cyan font-bold' : 'text-text-muted'">
|
|
|
|
|
{{ fight.botB?.name || '???' }}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-neon-pink text-[10px] font-pixel">R{{ fight.totalRounds }}</span>
|
|
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-else>
|
|
|
|
|
<p class="font-pixel text-text-muted text-xs italic">
|
|
|
|
|
The ring is empty. Be the first.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
2026-03-07 10:42:18 +00:00
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.crowd-bob {
|
|
|
|
|
animation: crowdBob 3s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes crowdBob {
|
|
|
|
|
0%, 100% { transform: translateY(0); }
|
|
|
|
|
50% { transform: translateY(-6px); }
|
|
|
|
|
}
|
|
|
|
|
</style>
|