import {memo, useCallback, useRef} from 'react'; import {DamageTypes} from '@/ecs/components/vulnerable.js'; import {useEcsTick} from '@/react/context/ecs.js'; import useAnimationFrame from '@/react/hooks/use-animation-frame.js'; import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js'; import styles from './damages.module.css'; function damageHue(type) { let hue; switch(type) { case DamageTypes.PAIN: { hue = [0, 30]; break; } case DamageTypes.HEALING: { hue = [90, 120]; break; } case DamageTypes.MANA: { hue = [215, 220]; break; } } return hue; } class Damage { elapsed = 0; hue = [0, 30]; offsetX = 0; offsetY = 0; step = 0; constructor() { const element = document.createElement('div'); element.classList.add(styles.damage); element.appendChild(document.createElement('p')); this.element = element; } reset(key, scale, {amount, position, type}) { this.elapsed = 0; [this.hueStart, this.hueEnd] = damageHue(type); this.offsetX = 20 * scale * 1 * (Math.random() - 0.5); this.offsetY = -1 * (10 + Math.random() * 45 * scale); this.step = 0; const {element} = this; element.style.setProperty('rotate', `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`); element.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount))))); element.style.setProperty('--positionX', `${position.x * scale}px`); element.style.setProperty('--positionY', `${position.y * scale}px`); element.style.setProperty('zIndex', key); const p = element.querySelector('p'); p.style.scale = scale / 2; p.innerText = Math.abs(amount); } tick(elapsed, stepSize) { this.elapsed += elapsed; this.step += elapsed; if (this.step > stepSize) { this.step = this.step % stepSize; // offset let offsetX = 0, offsetY = 0; if (this.elapsed <= 0.5) { offsetX = easeOutQuad(this.elapsed, 0, this.offsetX, 0.5); offsetY = easeOutQuad(this.elapsed, 0, this.offsetY, 0.5); } else if (this.elapsed > 1.375) { offsetX = easeOutQuad(this.elapsed - 1.375, this.offsetX, -this.offsetX, 0.125); offsetY = easeOutQuad(this.elapsed - 1.375, this.offsetY, -this.offsetY, 0.125); } else { offsetX = this.offsetX; offsetY = this.offsetY; } this.element.style.setProperty('--offsetX', offsetX); this.element.style.setProperty('--offsetY', offsetY); // scale let scale = 0.35; if (this.elapsed <= 0.5) { scale = easeOutQuad(this.elapsed, 0, 1, 0.5); } else if (this.elapsed > 0.5 && this.elapsed < 0.6) { scale = linear(this.elapsed - 0.5, 1, 0.5, 0.1); } else if (this.elapsed > 0.6 && this.elapsed < 0.675) { scale = easeInQuint(this.elapsed - 0.6, 1, 0.25, 0.075); } else if (this.elapsed > 0.675 && this.elapsed < 1.375) { scale = 1; } else if (this.elapsed > 1.375) { scale = easeOutQuad(this.elapsed - 1.375, 1, -1, 0.125); } this.element.style.setProperty('--scale', scale); // opacity let opacity = 0.75; if (this.elapsed <= 0.375) { opacity = linear(this.elapsed, 0.75, 0.25, 0.375); } else if (this.elapsed > 0.375 && this.elapsed < 1.375) { opacity = 1; } else if (this.elapsed > 1.375) { opacity = linear(this.elapsed - 1.375, 1, -1, 0.125); } this.element.style.setProperty('--opacity', opacity); // hue this.element.style.setProperty( '--hue', easeInOutExpo( Math.abs((this.elapsed % 0.3) - 0.15) / 0.15, this.hueStart, this.hueEnd, 1, ), ); } } } function Damages({scale}) { const damages = useRef({}); const damagesRef = useRef(); const pool = useRef([]); const onEcsTick = useCallback((payload) => { for (const id in payload) { const update = payload[id]; if (false === update || !update.Vulnerable?.damage) { continue; } const {damage: damageUpdate} = update.Vulnerable; for (const key in damageUpdate) { const damage = pool.current.length > 0 ? pool.current.pop() : new Damage(); damage.reset(key, scale, damageUpdate[key]); damages.current[key] = damage; damagesRef.current.appendChild(damage.element); } } }, [scale]); useEcsTick(onEcsTick); const frame = useCallback((elapsed) => { if (!damagesRef.current) { return; } const keys = Object.keys(damages.current); if (0 === keys.length && 0 === pool.current.length) { for (let i = 0; i < 500; ++i) { pool.current.push(new Damage()); } } const stepSize = keys.length > 150 ? (keys.length / 500) * 0.25 : elapsed; for (const key of keys) { const damage = damages.current[key]; damage.tick(elapsed, stepSize); if (damage.elapsed > 1.5) { damagesRef.current.removeChild(damage.element); delete damages.current[key]; if (pool.current.length < 1000) { pool.current.push(damage); } } } }, []); useAnimationFrame(frame); return (
); } export default memo(Damages);