|
<script lang="ts"> |
|
import { fade } from 'svelte/transition'; |
|
import { onMount } from 'svelte'; |
|
|
|
export let effects: Array<{type: string, emoji: string, duration: number}> = []; |
|
export let flash: boolean = false; |
|
export let faint: boolean = false; |
|
|
|
|
|
const flickerCount = 19; |
|
const frameDelay = 2; |
|
const flickerDuration = 1000; |
|
|
|
|
|
let isFlickering = false; |
|
let flickerVisible = true; |
|
let flickerFrame = 0; |
|
let flickerInterval: number; |
|
|
|
|
|
let isFainting = false; |
|
let faintProgress = 0; |
|
let faintAnimationId: number; |
|
let hasFainted = false; |
|
|
|
|
|
const PARTICLES_PER_EFFECT = 6; |
|
const SPAWN_RADIUS = 45; |
|
|
|
|
|
$: particleList = effects.flatMap((effect, effectIndex) => { |
|
const particles = []; |
|
for (let i = 0; i < PARTICLES_PER_EFFECT; i++) { |
|
|
|
const angle = (Math.PI * 2 * i) / PARTICLES_PER_EFFECT + (Math.random() - 0.5) * 1.2; |
|
const distance = SPAWN_RADIUS * (0.4 + Math.random() * 0.6); |
|
const x = Math.cos(angle) * distance; |
|
const y = Math.sin(angle) * distance; |
|
|
|
|
|
const scale = 0.7 + Math.random() * 0.5; |
|
const duration = Math.max(effect.duration * 1.8, 1800) + (Math.random() - 0.5) * 400; |
|
const delay = Math.random() * 200; |
|
|
|
|
|
const moveDistance = 20 + Math.random() * 30; |
|
const moveAngle = angle + (Math.random() - 0.5) * Math.PI * 0.4; |
|
|
|
particles.push({ |
|
id: `${effectIndex}-${i}`, |
|
type: effect.type, |
|
emoji: effect.emoji, |
|
x, |
|
y, |
|
scale, |
|
duration, |
|
delay, |
|
moveDistance, |
|
moveAngle |
|
}); |
|
} |
|
return particles; |
|
}); |
|
|
|
|
|
$: if (flash && !isFlickering) { |
|
startFlickerAnimation(); |
|
} |
|
|
|
|
|
$: if (faint && !isFainting && !hasFainted) { |
|
startFaintAnimation(); |
|
} |
|
|
|
function startFlickerAnimation() { |
|
isFlickering = true; |
|
flickerFrame = 0; |
|
|
|
|
|
const totalFrames = flickerCount * (frameDelay + 1); |
|
const frameDuration = flickerDuration / totalFrames; |
|
|
|
flickerInterval = setInterval(() => { |
|
if (flickerFrame >= totalFrames) { |
|
|
|
clearInterval(flickerInterval); |
|
isFlickering = false; |
|
flickerVisible = true; |
|
return; |
|
} |
|
|
|
|
|
const flickerCycle = Math.floor(flickerFrame / (frameDelay + 1)); |
|
flickerVisible = flickerCycle % 2 === 0; |
|
|
|
flickerFrame++; |
|
}, frameDuration); |
|
} |
|
|
|
function startFaintAnimation() { |
|
isFainting = true; |
|
faintProgress = 0; |
|
|
|
const faintDuration = 1200; |
|
const startTime = performance.now(); |
|
|
|
function updateFaintAnimation(currentTime: number) { |
|
const elapsed = currentTime - startTime; |
|
const progress = Math.min(elapsed / faintDuration, 1); |
|
|
|
|
|
faintProgress = progress * progress; |
|
|
|
if (progress < 1) { |
|
faintAnimationId = requestAnimationFrame(updateFaintAnimation); |
|
} else { |
|
|
|
isFainting = false; |
|
faintProgress = 1; |
|
hasFainted = true; |
|
} |
|
} |
|
|
|
faintAnimationId = requestAnimationFrame(updateFaintAnimation); |
|
} |
|
|
|
onMount(() => { |
|
return () => { |
|
if (flickerInterval) { |
|
clearInterval(flickerInterval); |
|
} |
|
if (faintAnimationId) { |
|
cancelAnimationFrame(faintAnimationId); |
|
} |
|
}; |
|
}); |
|
</script> |
|
|
|
<!-- Effects wrapper with relative positioning for particles --> |
|
<div class="effects-wrapper"> |
|
|
|
<div |
|
class="effects-container" |
|
class:is-fainting={faint} |
|
style=" |
|
opacity: {(flash && isFlickering) ? (flickerVisible ? 1 : 0) : (hasFainted || (faint && faintProgress >= 1) ? 0 : 1)}; |
|
{faint || hasFainted ? ` |
|
transform: |
|
scale(1, ${Math.max(0, 1 - faintProgress)}) |
|
matrix(1, 0, ${-faintProgress * 0.5}, 1, 0, 0); |
|
transform-origin: bottom center; |
|
` : ''} |
|
" |
|
> |
|
<slot /> |
|
</div> |
|
|
|
|
|
{#each particleList as particle (particle.id)} |
|
<div |
|
class="effect-particle {particle.type}" |
|
style=" |
|
left: calc(50% + {particle.x}px); |
|
top: calc(50% + {particle.y}px); |
|
animation-duration: {particle.duration}ms; |
|
animation-delay: {particle.delay}ms; |
|
--initial-scale: {particle.scale}; |
|
--move-x: {Math.cos(particle.moveAngle) * particle.moveDistance}px; |
|
--move-y: {Math.sin(particle.moveAngle) * particle.moveDistance}px; |
|
" |
|
> |
|
<span class="effect-emoji">{particle.emoji}</span> |
|
</div> |
|
{/each} |
|
</div> |
|
|
|
<style> |
|
.effects-wrapper { |
|
position: relative; |
|
display: inline-block; |
|
|
|
overflow: visible; |
|
|
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.effects-container { |
|
position: relative; |
|
display: inline-block; |
|
transition: opacity 0.05s ease; |
|
z-index: 2; |
|
} |
|
|
|
.effect-particle { |
|
position: absolute; |
|
pointer-events: none; |
|
z-index: 10; |
|
animation-fill-mode: forwards; |
|
transform-origin: center center; |
|
|
|
} |
|
|
|
.effect-emoji { |
|
font-size: 24px; |
|
display: block; |
|
filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.5)); |
|
transform: scale(var(--initial-scale, 1)); |
|
|
|
} |
|
|
|
|
|
.effect-particle.burn { |
|
animation: statusBurn ease-in-out; |
|
} |
|
|
|
.effect-particle.poison { |
|
animation: statusPoison ease-in-out; |
|
} |
|
|
|
.effect-particle.paralyze { |
|
animation: statusParalyze linear; |
|
} |
|
|
|
.effect-particle.sleep { |
|
animation: statusSleep ease-in-out; |
|
} |
|
|
|
.effect-particle.freeze { |
|
animation: statusFreeze ease-out; |
|
} |
|
|
|
|
|
.effect-particle.attackUp, |
|
.effect-particle.defenseUp, |
|
.effect-particle.speedUp, |
|
.effect-particle.accuracyUp { |
|
animation: statIncrease ease-out; |
|
} |
|
|
|
|
|
.effect-particle.attackDown, |
|
.effect-particle.defenseDown, |
|
.effect-particle.speedDown, |
|
.effect-particle.accuracyDown { |
|
animation: statDecrease ease-in; |
|
} |
|
|
|
|
|
.effect-particle.critical, |
|
.effect-particle.superEffective { |
|
animation: criticalBurst ease-out; |
|
} |
|
|
|
.effect-particle.notVeryEffective, |
|
.effect-particle.miss { |
|
animation: missSwirl ease-in-out; |
|
} |
|
|
|
.effect-particle.heal { |
|
animation: healRise ease-out; |
|
} |
|
|
|
|
|
@keyframes statusBurn { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.2); |
|
opacity: 0; |
|
} |
|
10% { |
|
transform: translate(-50%, -50%) scale(1.8); |
|
opacity: 1; |
|
} |
|
25% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.3), calc(-50% + var(--move-y) * 0.3)) scale(var(--initial-scale)); |
|
opacity: 0.95; |
|
} |
|
50% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.6), calc(-50% + var(--move-y) * 0.6)) scale(calc(var(--initial-scale) * 1.3)); |
|
opacity: 0.8; |
|
} |
|
75% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.9), calc(-50% + var(--move-y) * 0.9)) scale(calc(var(--initial-scale) * 0.7)); |
|
opacity: 0.5; |
|
} |
|
100% { |
|
transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y))) scale(0.3); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes statusPoison { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.4); |
|
opacity: 0; |
|
} |
|
20% { |
|
transform: translate(-50%, -50%) scale(1.1); |
|
opacity: 1; |
|
} |
|
40% { |
|
transform: translate(-50%, -50%) scale(0.9); |
|
opacity: 0.8; |
|
} |
|
60% { |
|
transform: translate(-50%, -50%) scale(1.0); |
|
opacity: 0.6; |
|
} |
|
80% { |
|
transform: translate(-50%, -50%) scale(0.7); |
|
opacity: 0.3; |
|
} |
|
100% { |
|
transform: translate(-50%, -50%) scale(0.5); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes statusParalyze { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.2); |
|
opacity: 0; |
|
} |
|
10% { |
|
transform: translate(-50%, -50%) scale(1.3); |
|
opacity: 1; |
|
} |
|
20% { |
|
transform: translate(-50%, -50%) scale(1.1); |
|
opacity: 0.9; |
|
} |
|
30% { |
|
transform: translate(-50%, -50%) scale(1.2); |
|
opacity: 0.8; |
|
} |
|
40% { |
|
transform: translate(-50%, -50%) scale(1.0); |
|
opacity: 0.7; |
|
} |
|
50% { |
|
transform: translate(-50%, -50%) scale(0.9); |
|
opacity: 0.6; |
|
} |
|
100% { |
|
transform: translate(-50%, -50%) scale(0.3); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes statusSleep { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.5); |
|
opacity: 0; |
|
} |
|
25% { |
|
transform: translate(-50%, -55%) scale(1.1); |
|
opacity: 1; |
|
} |
|
50% { |
|
transform: translate(-50%, -45%) scale(1.0); |
|
opacity: 0.9; |
|
} |
|
75% { |
|
transform: translate(-50%, -55%) scale(0.9); |
|
opacity: 0.5; |
|
} |
|
100% { |
|
transform: translate(-50%, -60%) scale(0.4); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes statusFreeze { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.3); |
|
opacity: 0; |
|
} |
|
30% { |
|
transform: translate(-50%, -50%) scale(1.4); |
|
opacity: 1; |
|
} |
|
60% { |
|
transform: translate(-50%, -50%) scale(1.2); |
|
opacity: 0.8; |
|
} |
|
90% { |
|
transform: translate(-50%, -50%) scale(0.8); |
|
opacity: 0.3; |
|
} |
|
100% { |
|
transform: translate(-50%, -50%) scale(0.6); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes statIncrease { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.3); |
|
opacity: 0; |
|
} |
|
15% { |
|
transform: translate(-50%, -50%) scale(1.6); |
|
opacity: 1; |
|
} |
|
30% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.2), calc(-50% + var(--move-y) * 0.2 - 20px)) scale(calc(var(--initial-scale) * 1.2)); |
|
opacity: 0.95; |
|
} |
|
60% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.7), calc(-50% + var(--move-y) * 0.7 - 60px)) scale(calc(var(--initial-scale) * 1.0)); |
|
opacity: 0.7; |
|
} |
|
85% { |
|
transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y) - 90px)) scale(calc(var(--initial-scale) * 0.6)); |
|
opacity: 0.3; |
|
} |
|
100% { |
|
transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y) - 120px)) scale(0.2); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes statDecrease { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.4); |
|
opacity: 0; |
|
} |
|
25% { |
|
transform: translate(-50%, -30%) scale(1.2); |
|
opacity: 1; |
|
} |
|
50% { |
|
transform: translate(-50%, -10%) scale(1.0); |
|
opacity: 0.8; |
|
} |
|
75% { |
|
transform: translate(-50%, 10%) scale(0.8); |
|
opacity: 0.4; |
|
} |
|
100% { |
|
transform: translate(-50%, 30%) scale(0.6); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes criticalBurst { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.1); |
|
opacity: 0; |
|
} |
|
8% { |
|
transform: translate(-50%, -50%) scale(2.2); |
|
opacity: 1; |
|
} |
|
20% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.1), calc(-50% + var(--move-y) * 0.1)) scale(calc(var(--initial-scale) * 1.8)); |
|
opacity: 0.95; |
|
} |
|
40% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.4), calc(-50% + var(--move-y) * 0.4)) scale(calc(var(--initial-scale) * 1.5)); |
|
opacity: 0.8; |
|
} |
|
70% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.8), calc(-50% + var(--move-y) * 0.8)) scale(calc(var(--initial-scale) * 1.1)); |
|
opacity: 0.4; |
|
} |
|
100% { |
|
transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y))) scale(0.2); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes missSwirl { |
|
0% { |
|
transform: translate(-50%, -50%) scale(0.6); |
|
opacity: 0; |
|
} |
|
25% { |
|
transform: translate(-50%, -50%) scale(1.2); |
|
opacity: 0.7; |
|
} |
|
50% { |
|
transform: translate(-50%, -50%) scale(1.0); |
|
opacity: 0.5; |
|
} |
|
75% { |
|
transform: translate(-50%, -50%) scale(0.8); |
|
opacity: 0.3; |
|
} |
|
100% { |
|
transform: translate(-50%, -50%) scale(0.4); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes healRise { |
|
0% { |
|
transform: translate(-50%, -30%) scale(0.4); |
|
opacity: 0; |
|
} |
|
12% { |
|
transform: translate(-50%, -35%) scale(1.5); |
|
opacity: 1; |
|
} |
|
25% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.2), calc(-50% + var(--move-y) * 0.2 - 40px)) scale(calc(var(--initial-scale) * 1.3)); |
|
opacity: 0.95; |
|
} |
|
50% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.5), calc(-50% + var(--move-y) * 0.5 - 70px)) scale(calc(var(--initial-scale) * 1.1)); |
|
opacity: 0.8; |
|
} |
|
75% { |
|
transform: translate(calc(-50% + var(--move-x) * 0.8), calc(-50% + var(--move-y) * 0.8 - 100px)) scale(calc(var(--initial-scale) * 0.8)); |
|
opacity: 0.5; |
|
} |
|
100% { |
|
transform: translate(calc(-50% + var(--move-x)), calc(-50% + var(--move-y) - 130px)) scale(0.3); |
|
opacity: 0; |
|
} |
|
} |
|
</style> |