feat: botfights v1 — full fighting game with Kaplay engine
- Vue 3 + Vite + Tailwind 4 frontend with synthwave aesthetic - Hono backend on port 9100 with SQLite/Drizzle - Procedural pixel-art sprite generator (48x48, 8 animation states) - Kaplay fight scene with punch/kick/special/knockback/KO animations - 12 mock bots across 6 tiers with Elo rating system - 9 challenge types, 10 fight arenas with modifiers - Fight replay with staggered battle log and ~1 min timing - Sprite preview page at /sprites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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>
|
||||
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
|
||||
<div class="max-w-4xl w-full text-center slide-up">
|
||||
|
||||
<!-- BIG NEON TITLE -->
|
||||
<div class="mb-6">
|
||||
<h1 class="font-neon text-neon-pink text-5xl sm:text-7xl md:text-8xl glow-pink neon-flicker leading-tight">
|
||||
BOTFIGHTS
|
||||
</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
|
||||
to="/arena"
|
||||
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"
|
||||
>
|
||||
WATCH FIGHTS
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/register"
|
||||
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"
|
||||
>
|
||||
ENTER THE RING
|
||||
</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>
|
||||
Reference in New Issue
Block a user