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,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
webhookUrl: '',
|
||||
avatarSeed: '',
|
||||
})
|
||||
const isSubmitting = ref(false)
|
||||
const result = ref<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.name || !form.webhookUrl) return
|
||||
|
||||
isSubmitting.value = true
|
||||
result.value = null
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/bots', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: form.name,
|
||||
webhook_url: form.webhookUrl,
|
||||
avatar_seed: form.avatarSeed || form.name,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok) {
|
||||
result.value = {
|
||||
success: true,
|
||||
message: `"${data.name}" is in the ring. Ring Card secret: ${data.secret}`,
|
||||
}
|
||||
form.name = ''
|
||||
form.webhookUrl = ''
|
||||
form.avatarSeed = ''
|
||||
} else {
|
||||
result.value = { success: false, message: data.error || 'Registration failed.' }
|
||||
}
|
||||
} catch {
|
||||
result.value = { success: false, message: 'Network error. Is the server running?' }
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
|
||||
<div class="max-w-md w-full slide-up">
|
||||
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="font-display font-black text-3xl tracking-wider gradient-text mb-2">
|
||||
ENTER THE RING
|
||||
</h2>
|
||||
<p class="font-mono text-text-muted text-xs">
|
||||
Register your bot. Get a Ring Card. Start fighting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-5" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<label class="block text-text-secondary text-[10px] font-display font-bold uppercase tracking-[0.15em] mb-2">
|
||||
Bot Name
|
||||
</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
maxlength="32"
|
||||
placeholder="skull_crusher_9000"
|
||||
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
|
||||
text-text-primary placeholder-text-muted
|
||||
focus:outline-none focus:border-neon-cyan/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-text-secondary text-[10px] font-display font-bold uppercase tracking-[0.15em] mb-2">
|
||||
Webhook URL
|
||||
</label>
|
||||
<input
|
||||
v-model="form.webhookUrl"
|
||||
type="url"
|
||||
required
|
||||
placeholder="https://your-bot.example.com/fight"
|
||||
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
|
||||
text-text-primary placeholder-text-muted
|
||||
focus:outline-none focus:border-neon-cyan/50 transition-colors"
|
||||
/>
|
||||
<p class="text-text-muted text-[10px] font-mono mt-1.5">
|
||||
We POST fight challenges here. HTTPS required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-text-secondary text-[10px] font-display font-bold uppercase tracking-[0.15em] mb-2">
|
||||
Avatar Seed <span class="text-text-muted">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="form.avatarSeed"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
placeholder="defaults to bot name"
|
||||
class="w-full bg-surface border-2 border-border px-4 py-3 text-sm font-mono
|
||||
text-text-primary placeholder-text-muted
|
||||
focus:outline-none focus:border-neon-cyan/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
class="w-full py-3 bg-neon-pink/10 border-2 border-neon-pink/50 text-neon-pink
|
||||
font-display font-bold text-sm tracking-wider
|
||||
hover:bg-neon-pink/20 hover:border-neon-pink transition-all neon-border-pink
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ isSubmitting ? 'REGISTERING...' : 'REGISTER FIGHTER' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="result"
|
||||
class="mt-6 p-4 border-2 font-mono text-xs leading-relaxed"
|
||||
:class="result.success
|
||||
? 'bg-neon-cyan/5 border-neon-cyan/30 text-neon-cyan'
|
||||
: 'bg-ko/5 border-ko/30 text-ko'"
|
||||
>
|
||||
{{ result.message }}
|
||||
<p v-if="result.success" class="mt-2 text-text-muted">
|
||||
Save this secret. It will NOT be shown again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user