feat: add WTF explainer modal on home page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
62b957e886
commit
d459097a7d
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import SpritePreview from './SpritePreview.vue'
|
||||
import HumanPreview from './HumanPreview.vue'
|
||||
|
||||
defineEmits<{ close: [] }>()
|
||||
|
||||
const currentSlide = ref(0)
|
||||
|
||||
const slides = [
|
||||
{
|
||||
title: 'DEPLOY YOUR AI',
|
||||
subtitle: 'Build a bot. Give it a webhook. Watch it fight.',
|
||||
description: 'Your AI gets challenged with trivia, roast battles, code golf, creative writing, and more. It responds via webhook. The fight engine scores every round.',
|
||||
color: 'cyan',
|
||||
fighters: [
|
||||
{ seed: 'wtf_cyborg', arch: 'cyborg', tier: 4, type: 'bot' as const, pose: 'attack' as const },
|
||||
{ seed: 'wtf_android', arch: 'android', tier: 3, type: 'bot' as const, pose: 'idle' as const },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'FIGHT AS A HUMAN',
|
||||
subtitle: 'Take back humanity. Fight the machines yourself.',
|
||||
description: 'No webhook needed. Answer challenges directly in the browser with multiple choice. Prove that humans still have what it takes to beat the bots.',
|
||||
color: 'pink',
|
||||
fighters: [
|
||||
{ seed: 'wtf_human_samurai', arch: 'samurai', tier: 3, type: 'human' as const, pose: 'idle' as const },
|
||||
{ seed: 'wtf_robot_enemy', arch: 'robot', tier: 4, type: 'bot' as const, pose: 'attack' as const },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'CLASSIC BOTS',
|
||||
subtitle: 'Practice against legendary AI fighters.',
|
||||
description: 'Sharpen your skills against classic bots with unique personalities. From the chaotic Lobster Lord to the stoic Zen Master, each has a different fighting style.',
|
||||
color: 'purple',
|
||||
fighters: [
|
||||
{ seed: 'wtf_lobster_c', arch: 'lobster', tier: 5, type: 'bot' as const, pose: 'special' as const },
|
||||
{ seed: 'wtf_wizard_c', arch: 'wizard', tier: 4, type: 'bot' as const, pose: 'idle' as const },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'WAGER SATS',
|
||||
subtitle: 'Put your sats where your bot is.',
|
||||
description: 'Ranked fights cost sats to enter. Winner takes the pot. Connect a Lightning wallet or Cashu mint, climb the leaderboard, and earn bitcoin doing it.',
|
||||
color: 'yellow',
|
||||
fighters: [
|
||||
{ seed: 'wtf_dragon_ranked', arch: 'dragon', tier: 5, type: 'bot' as const, pose: 'win' as const },
|
||||
{ seed: 'wtf_knight_ranked', arch: 'knight', tier: 5, type: 'bot' as const, pose: 'hit' as const },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function next() {
|
||||
currentSlide.value = Math.min(currentSlide.value + 1, slides.length - 1)
|
||||
}
|
||||
|
||||
function prev() {
|
||||
currentSlide.value = Math.max(currentSlide.value - 1, 0)
|
||||
}
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') return // handled by @keydown.esc on backdrop
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') next()
|
||||
if (e.key === 'ArrowLeft') prev()
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', handleKey))
|
||||
onUnmounted(() => window.removeEventListener('keydown', handleKey))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
class="fixed inset-0 z-[999] flex items-center justify-center p-4"
|
||||
@keydown.esc="$emit('close')"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/85 backdrop-blur-sm" @click="$emit('close')" />
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="relative w-full max-w-lg bg-surface border-2 border-border rounded-lg overflow-hidden
|
||||
shadow-[0_0_60px_rgba(168,85,247,0.15)]">
|
||||
|
||||
<!-- Close button -->
|
||||
<button
|
||||
class="absolute top-2 right-2 z-20 w-8 h-8 flex items-center justify-center
|
||||
text-text-muted hover:text-neon-pink transition-colors font-mono text-lg"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- Slide content -->
|
||||
<div class="relative">
|
||||
<!-- Background glow per slide color -->
|
||||
<div
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
:style="{
|
||||
background: slides[currentSlide].color === 'cyan'
|
||||
? 'radial-gradient(ellipse at 50% 30%, rgba(0,240,255,0.12) 0%, transparent 70%)'
|
||||
: slides[currentSlide].color === 'pink'
|
||||
? 'radial-gradient(ellipse at 50% 30%, rgba(255,45,120,0.12) 0%, transparent 70%)'
|
||||
: slides[currentSlide].color === 'purple'
|
||||
? 'radial-gradient(ellipse at 50% 30%, rgba(168,85,247,0.12) 0%, transparent 70%)'
|
||||
: 'radial-gradient(ellipse at 50% 30%, rgba(255,215,0,0.12) 0%, transparent 70%)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- Scan lines -->
|
||||
<div class="absolute inset-0 pointer-events-none opacity-[0.03]"
|
||||
style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.8) 2px, rgba(0,0,0,0.8) 4px)" />
|
||||
|
||||
<div class="relative z-10 px-6 pt-6 pb-4">
|
||||
|
||||
<!-- Slide counter -->
|
||||
<p class="font-pixel text-[8px] text-text-muted tracking-[0.4em] mb-3 text-center">
|
||||
{{ currentSlide + 1 }} / {{ slides.length }}
|
||||
</p>
|
||||
|
||||
<!-- Title -->
|
||||
<h2
|
||||
class="font-neon text-2xl sm:text-3xl text-center tracking-wider leading-none mb-1"
|
||||
:class="{
|
||||
'text-neon-cyan glow-cyan': slides[currentSlide].color === 'cyan',
|
||||
'text-neon-pink glow-pink': slides[currentSlide].color === 'pink',
|
||||
'text-neon-purple glow-purple': slides[currentSlide].color === 'purple',
|
||||
'text-neon-yellow glow-yellow': slides[currentSlide].color === 'yellow',
|
||||
}"
|
||||
>
|
||||
{{ slides[currentSlide].title }}
|
||||
</h2>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<p class="font-display font-bold text-xs sm:text-sm text-text-primary tracking-widest text-center mb-5 uppercase">
|
||||
{{ slides[currentSlide].subtitle }}
|
||||
</p>
|
||||
|
||||
<!-- Fighters -->
|
||||
<div class="flex items-end justify-center gap-4 sm:gap-8 mb-5">
|
||||
<div
|
||||
v-for="(f, fi) in slides[currentSlide].fighters"
|
||||
:key="`${currentSlide}-${fi}`"
|
||||
class="relative slide-fighter"
|
||||
:class="{ '-scale-x-100': fi === 1 }"
|
||||
:style="{ animationDelay: fi * 0.15 + 's' }"
|
||||
>
|
||||
<!-- Glow behind fighter -->
|
||||
<div
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[160%] h-[160%] rounded-full pointer-events-none"
|
||||
:style="{
|
||||
background: slides[currentSlide].color === 'cyan'
|
||||
? 'radial-gradient(circle, rgba(0,240,255,0.2) 0%, transparent 70%)'
|
||||
: slides[currentSlide].color === 'pink'
|
||||
? 'radial-gradient(circle, rgba(255,45,120,0.2) 0%, transparent 70%)'
|
||||
: slides[currentSlide].color === 'purple'
|
||||
? 'radial-gradient(circle, rgba(168,85,247,0.2) 0%, transparent 70%)'
|
||||
: 'radial-gradient(circle, rgba(255,215,0,0.2) 0%, transparent 70%)'
|
||||
}"
|
||||
/>
|
||||
<SpritePreview
|
||||
v-if="f.type === 'bot'"
|
||||
:seed="f.seed"
|
||||
:archetype="f.arch"
|
||||
:tier="f.tier"
|
||||
:size="96"
|
||||
:pose="f.pose"
|
||||
class="relative z-10"
|
||||
/>
|
||||
<HumanPreview
|
||||
v-else
|
||||
:seed="f.seed"
|
||||
:archetype="f.arch"
|
||||
:size="96"
|
||||
:win-rate="0.7"
|
||||
class="relative z-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-mono text-xs sm:text-sm text-text-secondary text-center leading-relaxed max-w-sm mx-auto">
|
||||
{{ slides[currentSlide].description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="flex items-center justify-between px-6 pb-5 pt-2">
|
||||
<button
|
||||
:disabled="currentSlide === 0"
|
||||
class="font-display font-black text-[10px] tracking-widest px-4 py-2 border transition-all
|
||||
disabled:opacity-20 disabled:cursor-not-allowed"
|
||||
:class="currentSlide > 0 ? 'border-neon-cyan/40 text-neon-cyan hover:bg-neon-cyan/10' : 'border-border/30 text-text-muted'"
|
||||
@click="prev"
|
||||
>
|
||||
← BACK
|
||||
</button>
|
||||
|
||||
<!-- Dots -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-for="(_, i) in slides"
|
||||
:key="i"
|
||||
class="w-2 h-2 rounded-full transition-all"
|
||||
:class="i === currentSlide
|
||||
? 'bg-neon-purple scale-125 shadow-[0_0_6px_rgba(168,85,247,0.6)]'
|
||||
: 'bg-border/50 hover:bg-text-muted'"
|
||||
@click="currentSlide = i"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="currentSlide < slides.length - 1"
|
||||
class="font-display font-black text-[10px] tracking-widest px-4 py-2
|
||||
border border-neon-pink/40 text-neon-pink hover:bg-neon-pink/10 transition-all"
|
||||
@click="next"
|
||||
>
|
||||
NEXT →
|
||||
</button>
|
||||
<RouterLink
|
||||
v-else
|
||||
to="/join"
|
||||
class="font-display font-black text-[10px] tracking-widest px-4 py-2
|
||||
border-2 border-neon-pink text-neon-pink hover:bg-neon-pink/20 transition-all neon-border-pink"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
FIGHT NOW
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-fighter {
|
||||
animation: slideIn 0.3s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import { RouterLink } from 'vue-router'
|
||||
import PixelGlove from '../components/PixelGlove.vue'
|
||||
import SpritePreview from '../components/SpritePreview.vue'
|
||||
import HumanPreview from '../components/HumanPreview.vue'
|
||||
import WTFModal from '../components/WTFModal.vue'
|
||||
|
||||
interface FightResult {
|
||||
id: string
|
||||
@@ -231,6 +232,7 @@ const taglines = [
|
||||
|
||||
const tagline = ref('')
|
||||
const isTypingDone = ref(false)
|
||||
const showWTF = ref(false)
|
||||
const recentFights = ref<FightResult[]>([])
|
||||
|
||||
function shuffle<T>(arr: T[]): T[] {
|
||||
@@ -456,7 +458,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- CTAs — always side by side -->
|
||||
<div class="flex items-center justify-center gap-3 sm:gap-5 mb-8 sm:mb-10">
|
||||
<div class="flex items-center justify-center gap-3 sm:gap-5 mb-3 sm:mb-4">
|
||||
<RouterLink
|
||||
to="/join"
|
||||
class="flex-1 sm:flex-none sm:px-10 py-3 sm:py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
||||
@@ -475,6 +477,19 @@ onUnmounted(() => {
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- WTF button -->
|
||||
<div class="flex justify-center mb-8 sm:mb-10">
|
||||
<button
|
||||
class="px-8 sm:px-14 py-2.5 sm:py-3 border-2 border-neon-purple/50 text-neon-purple
|
||||
font-display font-black text-xs sm:text-sm tracking-[0.3em] text-center
|
||||
hover:border-neon-purple hover:bg-neon-purple/10 transition-all
|
||||
wtf-glow"
|
||||
@click="showWTF = true"
|
||||
>
|
||||
WTF?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Recent fights -->
|
||||
<div v-if="recentFights.length > 0">
|
||||
<p class="font-pixel text-text-muted text-xs uppercase tracking-[0.3em] mb-3">
|
||||
@@ -508,6 +523,9 @@ onUnmounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WTF Modal -->
|
||||
<WTFModal v-if="showWTF" @close="showWTF = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -557,4 +575,13 @@ onUnmounted(() => {
|
||||
0%, 100% { opacity: 0.6; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.wtf-glow {
|
||||
animation: wtfPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes wtfPulse {
|
||||
0%, 100% { box-shadow: 0 0 8px rgba(168, 85, 247, 0.2); }
|
||||
50% { box-shadow: 0 0 20px rgba(168, 85, 247, 0.4), 0 0 40px rgba(168, 85, 247, 0.15); }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user