|
<script lang="ts"> |
|
import { onMount } from 'svelte'; |
|
import type { PicletInstance } from '$lib/db/schema'; |
|
import { TYPE_DATA } from '$lib/types/picletTypes'; |
|
|
|
interface Props { |
|
instance: PicletInstance; |
|
onClose: () => void; |
|
} |
|
|
|
let { instance, onClose }: Props = $props(); |
|
let selectedTab = $state<'about' | 'abilities'>('about'); |
|
let showCelebration = $state(true); |
|
|
|
|
|
const updatedInstance = $derived(recalculatePicletStats(instance)); |
|
|
|
|
|
const battleDefinition = $derived(picletInstanceToBattleDefinition(updatedInstance)); |
|
|
|
|
|
const xpProgress = $derived(getXpProgress(updatedInstance.level, updatedInstance.xp)); |
|
const xpToNext = $derived(getXpTowardsNextLevel(updatedInstance.level, updatedInstance.xp)); |
|
const isMaxLevel = $derived(updatedInstance.level >= 100); |
|
|
|
|
|
const typeData = $derived(TYPE_DATA[updatedInstance.primaryType]); |
|
const tier = $derived(() => { |
|
const bst = updatedInstance.bst; |
|
if (bst >= 600) return { name: 'Legendary', color: '#FFD700', bg: 'linear-gradient(135deg, #FFD700, #FFA500)' }; |
|
if (bst >= 530) return { name: 'Elite', color: '#9932CC', bg: 'linear-gradient(135deg, #9932CC, #8A2BE2)' }; |
|
if (bst >= 480) return { name: 'Advanced', color: '#1E90FF', bg: 'linear-gradient(135deg, #1E90FF, #0066CC)' }; |
|
if (bst >= 420) return { name: 'Standard', color: '#32CD32', bg: 'linear-gradient(135deg, #32CD32, #228B22)' }; |
|
return { name: 'Basic', color: '#808080', bg: 'linear-gradient(135deg, #808080, #696969)' }; |
|
}); |
|
|
|
let celebrationTimeout: NodeJS.Timeout; |
|
|
|
onMount(() => { |
|
|
|
celebrationTimeout = setTimeout(() => { |
|
showCelebration = false; |
|
}, 3000); |
|
|
|
return () => { |
|
if (celebrationTimeout) clearTimeout(celebrationTimeout); |
|
}; |
|
}); |
|
|
|
function dismissCelebration() { |
|
showCelebration = false; |
|
if (celebrationTimeout) clearTimeout(celebrationTimeout); |
|
} |
|
|
|
function handleContainerClick(event: MouseEvent) { |
|
event.stopPropagation(); |
|
} |
|
</script> |
|
|
|
<div class="detail-overlay" role="dialog" aria-modal="true" tabindex="-1" onclick={onClose} onkeydown={(e) => e.key === 'Escape' ? onClose() : null}> |
|
<div class="detail-container newly-caught" role="document" onclick={handleContainerClick}> |
|
|
|
|
|
{#if showCelebration} |
|
<div class="celebration-overlay" role="button" tabindex="0" onclick={dismissCelebration} onkeydown={(e) => e.key === 'Enter' || e.key === ' ' ? dismissCelebration() : null}> |
|
<div class="celebration-content"> |
|
<div class="celebration-sparkles">✨</div> |
|
<h1 class="celebration-title">Welcome to your team!</h1> |
|
<div class="celebration-piclet-name">{updatedInstance.nickname}</div> |
|
<p class="celebration-subtitle">Your first Piclet has been caught!</p> |
|
<div class="celebration-sparkles">🎉</div> |
|
<div class="tap-to-continue">Tap to continue</div> |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
|
|
<div class="detail-header" style="background: {tier().bg}"> |
|
<button class="close-button" onclick={onClose}>×</button> |
|
<div class="newly-caught-badge"> |
|
<span class="badge-text">✨ NEWLY CAUGHT ✨</span> |
|
</div> |
|
<div class="header-content"> |
|
<div class="piclet-image-container"> |
|
<img |
|
src={updatedInstance.imageUrl} |
|
alt={updatedInstance.nickname} |
|
class="piclet-image" |
|
/> |
|
<div class="golden-glow"></div> |
|
</div> |
|
<div class="piclet-info"> |
|
<h1 class="piclet-name">{updatedInstance.nickname}</h1> |
|
<div class="piclet-meta"> |
|
<div class="type-badge" style="background: {typeData.color}"> |
|
{typeData.icon} {updatedInstance.primaryType} |
|
</div> |
|
<div class="tier-badge" style="color: {tier().color}"> |
|
{tier().name} |
|
</div> |
|
<div class="level-badge">Lv. {updatedInstance.level}</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-navigation"> |
|
<button |
|
class="tab-button" |
|
class:active={selectedTab === 'about'} |
|
onclick={() => selectedTab = 'about'} |
|
> |
|
About |
|
</button> |
|
<button |
|
class="tab-button" |
|
class:active={selectedTab === 'abilities'} |
|
onclick={() => selectedTab = 'abilities'} |
|
> |
|
Abilities |
|
</button> |
|
</div> |
|
|
|
|
|
<div class="detail-content"> |
|
{#if selectedTab === 'about'} |
|
<div class="about-tab"> |
|
<div class="description-section"> |
|
<h3>Description</h3> |
|
<p class="description-text">{updatedInstance.description}</p> |
|
</div> |
|
|
|
<div class="stats-section"> |
|
<h3>Combat Stats</h3> |
|
<div class="stats-grid"> |
|
<div class="stat-item"> |
|
<span class="stat-label">HP</span> |
|
<span class="stat-value">{updatedInstance.maxHp}</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Attack</span> |
|
<span class="stat-value">{updatedInstance.attack}</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Defense</span> |
|
<span class="stat-value">{updatedInstance.defense}</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-label">Speed</span> |
|
<span class="stat-value">{updatedInstance.speed}</span> |
|
</div> |
|
</div> |
|
<div class="bst-display"> |
|
<span class="bst-label">Base Stat Total:</span> |
|
<span class="bst-value" style="color: {tier().color}">{updatedInstance.bst}</span> |
|
</div> |
|
</div> |
|
|
|
<div class="xp-section"> |
|
<h3>Experience</h3> |
|
<div class="level-xp-section"> |
|
<div class="xp-bar-container"> |
|
<div class="xp-bar"> |
|
<div class="xp-fill" style="width: {xpProgress}%"></div> |
|
</div> |
|
<div class="xp-text"> |
|
{#if isMaxLevel} |
|
<span>MAX LEVEL</span> |
|
{:else} |
|
<span>{xpToNext} XP to level {updatedInstance.level + 1}</span> |
|
{/if} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
{:else if selectedTab === 'abilities'} |
|
<div class="abilities-tab"> |
|
<div class="special-ability-section"> |
|
<h3>Special Ability</h3> |
|
{#if isSpecialAbilityUnlocked(updatedInstance)} |
|
<AbilityDisplay ability={battleDefinition.specialAbility} /> |
|
{:else} |
|
<div class="locked-ability"> |
|
<span class="lock-icon">🔒</span> |
|
<span class="locked-text">Unlocks at level {updatedInstance.specialAbilityUnlockLevel}</span> |
|
</div> |
|
{/if} |
|
</div> |
|
|
|
<div class="moves-section"> |
|
<h3>Moves</h3> |
|
<div class="moves-grid"> |
|
{#each updatedInstance.moves as move} |
|
<MoveDisplay {move} picletLevel={updatedInstance.level} /> |
|
{/each} |
|
</div> |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<style> |
|
.detail-overlay { |
|
position: fixed; |
|
inset: 0; |
|
background: rgba(0, 0, 0, 0.8); |
|
backdrop-filter: blur(4px); |
|
z-index: 2000; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 1rem; |
|
animation: fadeIn 0.3s ease-out; |
|
} |
|
|
|
.detail-container.newly-caught { |
|
background: white; |
|
border-radius: 24px; |
|
max-width: 500px; |
|
width: 100%; |
|
max-height: 90vh; |
|
overflow: hidden; |
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); |
|
animation: slideInUp 0.4s ease-out; |
|
position: relative; |
|
} |
|
|
|
|
|
.celebration-overlay { |
|
position: absolute; |
|
inset: 0; |
|
background: radial-gradient(circle, rgba(255, 215, 0, 0.95) 0%, rgba(255, 140, 0, 0.9) 100%); |
|
z-index: 100; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
animation: celebrationPulse 2s ease-in-out infinite; |
|
} |
|
|
|
.celebration-content { |
|
text-align: center; |
|
color: white; |
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.celebration-sparkles { |
|
font-size: 3rem; |
|
animation: sparkle 1.5s ease-in-out infinite; |
|
margin: 0.5rem 0; |
|
} |
|
|
|
.celebration-title { |
|
font-size: 2.5rem; |
|
font-weight: 800; |
|
margin: 1rem 0 0.5rem; |
|
animation: titleGlow 2s ease-in-out infinite; |
|
} |
|
|
|
.celebration-piclet-name { |
|
font-size: 2rem; |
|
font-weight: 700; |
|
margin: 0.5rem 0; |
|
text-transform: uppercase; |
|
letter-spacing: 2px; |
|
} |
|
|
|
.celebration-subtitle { |
|
font-size: 1.2rem; |
|
margin: 1rem 0; |
|
opacity: 0.9; |
|
} |
|
|
|
.tap-to-continue { |
|
font-size: 1rem; |
|
margin-top: 2rem; |
|
opacity: 0.8; |
|
animation: pulse 1.5s ease-in-out infinite; |
|
} |
|
|
|
|
|
.detail-header { |
|
position: relative; |
|
padding: 2rem 1.5rem 1.5rem; |
|
color: white; |
|
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.newly-caught-badge { |
|
position: absolute; |
|
top: 1rem; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
background: rgba(255, 255, 255, 0.2); |
|
backdrop-filter: blur(10px); |
|
border: 2px solid rgba(255, 255, 255, 0.3); |
|
border-radius: 20px; |
|
padding: 0.5rem 1rem; |
|
animation: badgeGlow 2s ease-in-out infinite; |
|
} |
|
|
|
.badge-text { |
|
font-weight: 700; |
|
font-size: 0.9rem; |
|
letter-spacing: 1px; |
|
} |
|
|
|
.close-button { |
|
position: absolute; |
|
top: 1rem; |
|
right: 1rem; |
|
background: rgba(255, 255, 255, 0.2); |
|
border: none; |
|
border-radius: 50%; |
|
width: 40px; |
|
height: 40px; |
|
color: white; |
|
font-size: 1.5rem; |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
transition: all 0.2s ease; |
|
z-index: 10; |
|
} |
|
|
|
.close-button:hover { |
|
background: rgba(255, 255, 255, 0.3); |
|
transform: scale(1.1); |
|
} |
|
|
|
.header-content { |
|
display: flex; |
|
align-items: center; |
|
gap: 1.5rem; |
|
margin-top: 2rem; |
|
} |
|
|
|
.piclet-image-container { |
|
position: relative; |
|
flex-shrink: 0; |
|
} |
|
|
|
.piclet-image { |
|
width: 100px; |
|
height: 100px; |
|
border-radius: 16px; |
|
object-fit: cover; |
|
border: 3px solid rgba(255, 255, 255, 0.3); |
|
position: relative; |
|
z-index: 2; |
|
} |
|
|
|
.golden-glow { |
|
position: absolute; |
|
inset: -10px; |
|
background: radial-gradient(circle, rgba(255, 215, 0, 0.6), transparent 70%); |
|
border-radius: 50%; |
|
animation: goldenGlow 2s ease-in-out infinite; |
|
z-index: 1; |
|
} |
|
|
|
.piclet-info { |
|
flex: 1; |
|
min-width: 0; |
|
} |
|
|
|
.piclet-name { |
|
font-size: 1.8rem; |
|
font-weight: 700; |
|
margin: 0 0 0.5rem; |
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.piclet-meta { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 0.5rem; |
|
} |
|
|
|
.type-badge, .tier-badge, .level-badge { |
|
padding: 0.25rem 0.75rem; |
|
border-radius: 20px; |
|
font-size: 0.8rem; |
|
font-weight: 600; |
|
text-transform: uppercase; |
|
letter-spacing: 0.5px; |
|
} |
|
|
|
.type-badge { |
|
color: white; |
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.tier-badge { |
|
background: rgba(255, 255, 255, 0.2); |
|
backdrop-filter: blur(10px); |
|
} |
|
|
|
.level-badge { |
|
background: rgba(255, 255, 255, 0.3); |
|
color: white; |
|
} |
|
|
|
|
|
.tab-navigation { |
|
display: flex; |
|
border-bottom: 1px solid #e0e0e0; |
|
background: #f8f9fa; |
|
} |
|
|
|
.tab-button { |
|
flex: 1; |
|
padding: 1rem; |
|
border: none; |
|
background: none; |
|
font-weight: 600; |
|
color: #666; |
|
cursor: pointer; |
|
transition: all 0.2s ease; |
|
position: relative; |
|
} |
|
|
|
.tab-button.active { |
|
color: #007bff; |
|
} |
|
|
|
.tab-button.active::after { |
|
content: ''; |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
right: 0; |
|
height: 3px; |
|
background: #007bff; |
|
} |
|
|
|
|
|
.detail-content { |
|
max-height: 60vh; |
|
overflow-y: auto; |
|
padding: 1.5rem; |
|
} |
|
|
|
.about-tab, .abilities-tab { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 1.5rem; |
|
} |
|
|
|
.description-section h3, |
|
.stats-section h3, |
|
.xp-section h3, |
|
.special-ability-section h3, |
|
.moves-section h3 { |
|
margin: 0 0 1rem; |
|
font-size: 1.2rem; |
|
font-weight: 600; |
|
color: #333; |
|
} |
|
|
|
.description-text { |
|
color: #666; |
|
line-height: 1.6; |
|
margin: 0; |
|
} |
|
|
|
.stats-grid { |
|
display: grid; |
|
grid-template-columns: repeat(2, 1fr); |
|
gap: 1rem; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.stat-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 0.75rem; |
|
background: #f8f9fa; |
|
border-radius: 8px; |
|
} |
|
|
|
.stat-label { |
|
font-weight: 600; |
|
color: #666; |
|
} |
|
|
|
.stat-value { |
|
font-weight: 700; |
|
color: #333; |
|
font-size: 1.1rem; |
|
} |
|
|
|
.bst-display { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 1rem; |
|
background: linear-gradient(135deg, #f8f9fa, #e9ecef); |
|
border-radius: 12px; |
|
border: 2px solid #dee2e6; |
|
} |
|
|
|
.bst-label { |
|
font-weight: 600; |
|
color: #666; |
|
} |
|
|
|
.bst-value { |
|
font-weight: 700; |
|
font-size: 1.3rem; |
|
} |
|
|
|
.level-xp-section { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
} |
|
|
|
.xp-bar-container { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
} |
|
|
|
.xp-bar { |
|
height: 8px; |
|
background: #e9ecef; |
|
border-radius: 4px; |
|
overflow: hidden; |
|
} |
|
|
|
.xp-fill { |
|
height: 100%; |
|
background: linear-gradient(90deg, #28a745, #20c997); |
|
transition: width 0.3s ease; |
|
} |
|
|
|
.xp-text { |
|
text-align: center; |
|
font-size: 0.9rem; |
|
color: #666; |
|
} |
|
|
|
.locked-ability { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
padding: 1rem; |
|
background: #f8f9fa; |
|
border: 2px dashed #dee2e6; |
|
border-radius: 12px; |
|
color: #666; |
|
font-style: italic; |
|
} |
|
|
|
.lock-icon { |
|
font-size: 1.2rem; |
|
} |
|
|
|
.moves-grid { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.75rem; |
|
} |
|
|
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; } |
|
to { opacity: 1; } |
|
} |
|
|
|
@keyframes slideInUp { |
|
from { |
|
opacity: 0; |
|
transform: translateY(100px) scale(0.9); |
|
} |
|
to { |
|
opacity: 1; |
|
transform: translateY(0) scale(1); |
|
} |
|
} |
|
|
|
@keyframes celebrationPulse { |
|
0%, 100% { transform: scale(1); } |
|
50% { transform: scale(1.02); } |
|
} |
|
|
|
@keyframes sparkle { |
|
0%, 100% { transform: rotate(0deg) scale(1); } |
|
25% { transform: rotate(-5deg) scale(1.1); } |
|
75% { transform: rotate(5deg) scale(1.1); } |
|
} |
|
|
|
@keyframes titleGlow { |
|
0%, 100% { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } |
|
50% { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5), 0 0 20px rgba(255, 255, 255, 0.8); } |
|
} |
|
|
|
@keyframes pulse { |
|
0%, 100% { opacity: 0.8; } |
|
50% { opacity: 1; } |
|
} |
|
|
|
@keyframes badgeGlow { |
|
0%, 100% { box-shadow: 0 0 10px rgba(255, 255, 255, 0.3); } |
|
50% { box-shadow: 0 0 20px rgba(255, 255, 255, 0.6), 0 0 30px rgba(255, 255, 255, 0.4); } |
|
} |
|
|
|
@keyframes goldenGlow { |
|
0%, 100% { opacity: 0.6; } |
|
50% { opacity: 0.9; } |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.detail-container { |
|
margin: 0.5rem; |
|
max-height: 95vh; |
|
} |
|
|
|
.celebration-title { |
|
font-size: 2rem; |
|
} |
|
|
|
.celebration-piclet-name { |
|
font-size: 1.5rem; |
|
} |
|
|
|
.header-content { |
|
flex-direction: column; |
|
text-align: center; |
|
gap: 1rem; |
|
} |
|
|
|
.stats-grid { |
|
grid-template-columns: 1fr; |
|
} |
|
} |
|
</style> |