import {useEffect,useRef,useState} from 'react'; import {Volume2,VolumeX,X} from 'lucide-react'; import type {Event} from '../lib/types'; import type {ResolvedRoll,CombatResult} from '../lib/combat'; import {dicePolygons} from '../lib/dice-mesh'; import {diceSoundEnabled,setDiceSoundEnabled} from '../lib/dice-audio'; export default function DiceResult({event,close}:{event:Event;close:()=>void}){ const details=event.details as (CombatResult&Partial&{physical?:boolean}),rolls=details?.rolls||(details?.dice?[{...details,label:'Roll'} as ResolvedRoll]:[]); const [animation,setAnimation]=useState({id:event.id,progress:window.matchMedia('(prefers-reduced-motion: reduce)').matches||!rolls.length?1:0}),[expanded,setExpanded]=useState(false),[sound,setSound]=useState(diceSoundEnabled),canvas=useRef(null),closeRef=useRef(close); closeRef.current=close; const progress=animation.id===event.id?animation.progress:0;const quickLabel=event.kind==='roll'?event.message.split(' · ').at(-1)?.split(':')[0]||'Roll':''; useEffect(()=>{const sync=()=>setSound(diceSoundEnabled());window.addEventListener('tablebound:sound',sync);window.addEventListener('storage',sync);return()=>{window.removeEventListener('tablebound:sound',sync);window.removeEventListener('storage',sync);};},[]); useEffect(()=>{ setExpanded(false);let frame=0;const media=window.matchMedia('(prefers-reduced-motion: reduce)'),start=performance.now(); const finish=()=>{cancelAnimationFrame(frame);setAnimation({id:event.id,progress:1});}; const animate=(now:number)=>{const t=Math.min(1,(now-start)/1100);setAnimation({id:event.id,progress:1-(1-t)**3});if(t<1)frame=requestAnimationFrame(animate);}; if(media.matches||!rolls.length)finish();else{setAnimation({id:event.id,progress:0});frame=requestAnimationFrame(animate);} const change=()=>{if(media.matches)finish();};media.addEventListener('change',change); return()=>{cancelAnimationFrame(frame);media.removeEventListener('change',change);}; },[event.id]); useEffect(()=>{if(progress<1||expanded)return;const timer=window.setTimeout(()=>closeRef.current(),6000);return()=>window.clearTimeout(timer);},[event.id,progress,expanded]); useEffect(()=>{ const el=canvas.current,ctx=el?.getContext('2d');if(!el||!ctx)return; const width=el.clientWidth||156,height=140,dpr=Math.min(2,window.devicePixelRatio||1);el.width=width*dpr;el.height=height*dpr;ctx.scale(dpr,dpr);ctx.clearRect(0,0,width,height); const dice=rolls.flatMap(r=>r.dice.map(value=>({value,sides:Number(r.expression.match(/d(\d+)/i)?.[1]||20)}))).slice(0,6),cols=Math.min(3,dice.length),rows=Math.ceil(dice.length/Math.max(1,cols)); dice.forEach((die,i)=>{ const cx=width*(i%cols+.5)/Math.max(1,cols),cy=height*(Math.floor(i/cols)+.5)/Math.max(1,rows),scale=dice.length===1?49:dice.length===2?31:24; ctx.save();ctx.translate(cx,cy-Math.sin(progress*Math.PI)*14);ctx.shadowColor='#000';ctx.shadowBlur=10;ctx.shadowOffsetY=6; const supported=[4,6,8,10,12,20].includes(die.sides); if(supported)for(const f of dicePolygons(die.sides,die.value,progress)){ ctx.beginPath();f.points.forEach(([x,y],j)=>j?ctx.lineTo(x*scale,-y*scale):ctx.moveTo(x*scale,-y*scale));ctx.closePath();ctx.fillStyle=`rgb(${Math.round(28+f.light*37)},${Math.round(48+f.light*41)},${Math.round(50+f.light*40)})`;ctx.fill();ctx.shadowBlur=0;ctx.shadowOffsetY=0;ctx.strokeStyle='#b9955c';ctx.lineWidth=1;ctx.stroke(); if(f.chosen&&progress===1){ctx.fillStyle='#fff2d1';ctx.font=`bold ${dice.length===1?28:18}px Georgia`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(String(die.value),f.center[0]*scale,-f.center[1]*scale);} } else{ctx.fillStyle='#d9c49b';ctx.font='bold 32px Georgia';ctx.textAlign='center';ctx.fillText(progress===1?String(die.value):'✧',0,0)} ctx.restore(); }); },[progress,event.id]); return ; }