feat: add WTF explainer modal on home page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 09:15:37 +00:00
co-authored by Claude Opus 4.6
parent 62b957e886
commit d459097a7d
2 changed files with 278 additions and 1 deletions
+250
View File
@@ -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')"
>
&times;
</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"
>
&larr; 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 &rarr;
</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>
+28 -1
View File
@@ -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>