Files
botfights/frontend/src/pages/SpritePreviewPage.vue
T
DorianandClaude Opus 4.6 335c148866 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>
2026-03-06 16:27:54 +00:00

79 lines
3.1 KiB
Vue

<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>