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,78 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { generateSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS } from '../game/sprites'
|
||||
|
||||
const currentFrame = ref(0)
|
||||
let intervalId: number | null = null
|
||||
|
||||
const tiers = [
|
||||
{ tier: 0, seed: 'lorem', label: 'Tier 0 - Unranked', desc: 'Clawbot' },
|
||||
{ tier: 1, seed: 'null', label: 'Tier 1 - Rookie', desc: 'Basic bot' },
|
||||
{ tier: 2, seed: 'deep', label: 'Tier 2 - Rising', desc: 'Belt + feet' },
|
||||
{ tier: 3, seed: 'quantum', label: 'Tier 3 - Contender', desc: 'Gloves + shoulders' },
|
||||
{ tier: 4, seed: 'skull', label: 'Tier 4 - Champion', desc: 'Headband + aura' },
|
||||
{ tier: 5, seed: 'architect', label: 'Tier 5 - Legend', desc: 'Crown + gold' },
|
||||
]
|
||||
|
||||
const animNames = Object.keys(ANIMATIONS) as (keyof typeof ANIMATIONS)[]
|
||||
const loadedImages: HTMLImageElement[] = []
|
||||
|
||||
onMounted(() => {
|
||||
for (const t of tiers) {
|
||||
const colors = getBotColors(t.seed)
|
||||
const dataUrl = generateSpriteSheet(t.seed, t.tier, colors.primary, colors.secondary)
|
||||
const img = new Image()
|
||||
img.src = dataUrl
|
||||
img.onload = () => renderAll()
|
||||
loadedImages.push(img)
|
||||
}
|
||||
|
||||
intervalId = window.setInterval(() => {
|
||||
currentFrame.value = (currentFrame.value + 1) % MAX_FRAMES
|
||||
renderAll()
|
||||
}, 180)
|
||||
})
|
||||
|
||||
onUnmounted(() => { if (intervalId) clearInterval(intervalId) })
|
||||
|
||||
function renderAll() {
|
||||
const canvases = document.querySelectorAll<HTMLCanvasElement>('.tier-preview')
|
||||
canvases.forEach((canvas, idx) => {
|
||||
if (idx >= loadedImages.length || !loadedImages[idx].complete) return
|
||||
const ctx = canvas.getContext('2d')!
|
||||
const displaySize = 128
|
||||
canvas.width = displaySize * animNames.length
|
||||
canvas.height = displaySize
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.imageSmoothingEnabled = false
|
||||
|
||||
animNames.forEach((anim, animIdx) => {
|
||||
const row = ANIMATIONS[anim].row
|
||||
const frames = ANIMATIONS[anim].frames
|
||||
const frame = currentFrame.value % frames
|
||||
ctx.drawImage(loadedImages[idx], frame * FRAME_SIZE, row * FRAME_SIZE, FRAME_SIZE, FRAME_SIZE, animIdx * displaySize, 0, displaySize, displaySize)
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 max-w-7xl mx-auto space-y-4">
|
||||
<h1 class="font-display font-black text-2xl text-neon-pink glow-pink tracking-wider">SPRITE TIER PREVIEW</h1>
|
||||
|
||||
<div class="flex gap-0">
|
||||
<div class="w-[110px] flex-shrink-0" />
|
||||
<div v-for="anim in animNames" :key="anim" class="flex-1 text-center font-pixel text-[8px] text-neon-cyan uppercase tracking-wider">{{ anim }}</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(t, idx) in tiers" :key="t.tier" class="border border-border rounded-lg bg-surface-raised/50 p-2 flex items-center gap-3">
|
||||
<div class="w-[110px] flex-shrink-0">
|
||||
<p class="font-display font-black text-xs tracking-wider" :class="`tier-${t.tier}`">{{ t.label }}</p>
|
||||
<p class="font-mono text-[8px] text-text-muted">{{ t.desc }}</p>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<canvas class="tier-preview block h-[128px]" style="image-rendering: pixelated;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user