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