piclets / src /lib /components /Battle /BattleEffects.svelte
Fraser's picture
cleaner animations
3c1b1fe
<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;
// GBA-style flicker animation parameters (matching original Snaplings timing)
const flickerCount = 19;
const frameDelay = 2;
const flickerDuration = 1000; // milliseconds - matches Snaplings original
// Flicker state management
let isFlickering = false;
let flickerVisible = true;
let flickerFrame = 0;
let flickerInterval: number;
// Faint animation state management
let isFainting = false;
let faintProgress = 0;
let faintAnimationId: number;
let hasFainted = false;
// Particle system configuration
const PARTICLES_PER_EFFECT = 6; // Number of emoji particles per effect
const SPAWN_RADIUS = 45; // Radius around piclet where particles spawn (reduced for better containment)
// Generate multiple particles for each effect
$: particleList = effects.flatMap((effect, effectIndex) => {
const particles = [];
for (let i = 0; i < PARTICLES_PER_EFFECT; i++) {
// More varied spawn positions for better coverage
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); // Tighter distance variation for better containment
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
// Enhanced animation properties for more dynamic effects (no rotation)
const scale = 0.7 + Math.random() * 0.5; // 0.7x to 1.2x initial size
const duration = Math.max(effect.duration * 1.8, 1800) + (Math.random() - 0.5) * 400; // Longer base duration
const delay = Math.random() * 200; // More staggered animation starts
// Additional movement properties for more dynamic motion (reduced range)
const moveDistance = 20 + Math.random() * 30; // Further reduced movement distance for better containment
const moveAngle = angle + (Math.random() - 0.5) * Math.PI * 0.4; // Further reduced movement angle variation
particles.push({
id: `${effectIndex}-${i}`,
type: effect.type,
emoji: effect.emoji,
x,
y,
scale,
duration,
delay,
moveDistance,
moveAngle
});
}
return particles;
});
// Watch for flash changes to trigger flicker animation
$: if (flash && !isFlickering) {
startFlickerAnimation();
}
// Watch for faint changes to trigger faint animation
$: if (faint && !isFainting && !hasFainted) {
startFaintAnimation();
}
function startFlickerAnimation() {
isFlickering = true;
flickerFrame = 0;
// Calculate frame duration based on total duration and frame count
const totalFrames = flickerCount * (frameDelay + 1);
const frameDuration = flickerDuration / totalFrames;
flickerInterval = setInterval(() => {
if (flickerFrame >= totalFrames) {
// Animation finished, always visible
clearInterval(flickerInterval);
isFlickering = false;
flickerVisible = true;
return;
}
// Toggle visibility every frameDelay frames
const flickerCycle = Math.floor(flickerFrame / (frameDelay + 1));
flickerVisible = flickerCycle % 2 === 0;
flickerFrame++;
}, frameDuration);
}
function startFaintAnimation() {
isFainting = true;
faintProgress = 0;
const faintDuration = 1200; // milliseconds - matches Snaplings original
const startTime = performance.now();
function updateFaintAnimation(currentTime: number) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / faintDuration, 1);
// Use easeIn curve for acceleration as it falls away
faintProgress = progress * progress;
if (progress < 1) {
faintAnimationId = requestAnimationFrame(updateFaintAnimation);
} else {
// Animation completed
isFainting = false;
faintProgress = 1; // Keep final state
hasFainted = true; // Mark as permanently fainted
}
}
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">
<!-- GBA-style flicker effect with faint animation -->
<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>
<!-- Multi-particle effects -->
{#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;
/* Ensure wrapper contains particles even during resize */
overflow: visible;
/* Create a proper containing block for particles */
width: 100%;
height: 100%;
}
.effects-container {
position: relative;
display: inline-block;
transition: opacity 0.05s ease;
z-index: 2; /* Ensure effects appear above platform (z-index: 0) */
}
.effect-particle {
position: absolute;
pointer-events: none;
z-index: 10;
animation-fill-mode: forwards;
transform-origin: center center;
/* Position relative to the Piclet 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));
/* Remove rotation - emojis stay upright */
}
/* Status effects - floating with rotation */
.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;
}
/* Stat increases - rising with spin */
.effect-particle.attackUp,
.effect-particle.defenseUp,
.effect-particle.speedUp,
.effect-particle.accuracyUp {
animation: statIncrease ease-out;
}
/* Stat decreases - falling with wobble */
.effect-particle.attackDown,
.effect-particle.defenseDown,
.effect-particle.speedDown,
.effect-particle.accuracyDown {
animation: statDecrease ease-in;
}
/* Special effects */
.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;
}
/* Complex multi-property animations */
@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>