import { nextTick, onScopeDispose, ref } from 'vue'; import { toast } from 'vue-sonner'; import { useRoomEmotes } from '@/composables/useRoomEmotes'; import { store as throwPaperBallRoute } from '@/routes/room/emote/paper-ball'; import type { PaperBallFlight, PaperBallThrowError, PaperBallThrownPayload, PaperBallTossOptions, } from '@/types/emote'; const PAPER_BALL_FLIGHT_DURATION = 780; const SEEN_EVENT_RETENTION = 5_000; const THROW_BUTTON_COOLDOWN = 550; export function usePaperBallToss({ roomId, currentPlayerId, board, }: PaperBallTossOptions) { const activePaperBalls = ref([]); const hitPlayerIds = ref([]); const throwingAtPlayerId = ref(null); const seenPaperBallIds = new Set(); const timers = new Set(); function schedule(callback: () => void, delay: number): void { const timer = window.setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); } async function throwPaperBall(targetPlayerId: number): Promise { if ( targetPlayerId === currentPlayerId() || throwingAtPlayerId.value !== null ) { return; } const csrfToken = document .querySelector('meta[name="csrf-token"]') ?.getAttribute('content'); if (!csrfToken) { toast.error('Der Papierknäuel konnte nicht geworfen werden.'); return; } throwingAtPlayerId.value = targetPlayerId; try { const response = await fetch(throwPaperBallRoute(roomId()).url, { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken, 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify({ target_player_id: targetPlayerId, }), }); if (response.redirected) { toast.error( 'Bitte lade den Raum neu und versuche es noch einmal.', ); return; } if (!response.ok) { const error = (await response .json() .catch(() => null)) as PaperBallThrowError | null; const message = error?.errors?.target_player_id?.[0] ?? error?.message ?? 'Der Papierknäuel konnte nicht geworfen werden.'; toast.error(message); } } catch { toast.error('Der Papierknäuel konnte nicht geworfen werden.'); } finally { schedule(() => { if (throwingAtPlayerId.value === targetPlayerId) { throwingAtPlayerId.value = null; } }, THROW_BUTTON_COOLDOWN); } } async function animatePaperBall( payload: PaperBallThrownPayload, ): Promise { if ( typeof payload.id !== 'string' || !Number.isInteger(payload.from_player_id) || !Number.isInteger(payload.target_player_id) || seenPaperBallIds.has(payload.id) ) { return; } seenPaperBallIds.add(payload.id); schedule( () => seenPaperBallIds.delete(payload.id), SEEN_EVENT_RETENTION, ); await nextTick(); const gameBoard = board.value; const fromAvatar = gameBoard?.querySelector( `[data-player-avatar="${payload.from_player_id}"]`, ); const targetAvatar = gameBoard?.querySelector( `[data-player-avatar="${payload.target_player_id}"]`, ); if (!gameBoard || !fromAvatar || !targetAvatar) { return; } const boardRect = gameBoard.getBoundingClientRect(); const fromRect = fromAvatar.getBoundingClientRect(); const targetRect = targetAvatar.getBoundingClientRect(); const startX = fromRect.left + fromRect.width / 2 - boardRect.left; const startY = fromRect.top + fromRect.height / 2 - boardRect.top; const travelX = targetRect.left + targetRect.width / 2 - boardRect.left - startX; const travelY = targetRect.top + targetRect.height / 2 - boardRect.top - startY; const distance = Math.hypot(travelX, travelY); const arcHeight = Math.min(130, Math.max(55, distance * 0.22)); activePaperBalls.value.push({ id: payload.id, style: { left: `${startX}px`, top: `${startY}px`, '--travel-quarter-x': `${travelX * 0.25}px`, '--travel-quarter-y': `${travelY * 0.25 - arcHeight * 0.75}px`, '--travel-mid-x': `${travelX * 0.5}px`, '--travel-mid-y': `${travelY * 0.5 - arcHeight}px`, '--travel-three-quarter-x': `${travelX * 0.75}px`, '--travel-three-quarter-y': `${travelY * 0.75 - arcHeight * 0.75}px`, '--travel-x': `${travelX}px`, '--travel-y': `${travelY}px`, }, }); schedule(() => { if (!hitPlayerIds.value.includes(payload.target_player_id)) { hitPlayerIds.value.push(payload.target_player_id); } }, PAPER_BALL_FLIGHT_DURATION - 80); schedule(() => { activePaperBalls.value = activePaperBalls.value.filter( (paperBall) => paperBall.id !== payload.id, ); }, PAPER_BALL_FLIGHT_DURATION + 40); schedule(() => { hitPlayerIds.value = hitPlayerIds.value.filter( (playerId) => playerId !== payload.target_player_id, ); }, PAPER_BALL_FLIGHT_DURATION + 420); } useRoomEmotes(roomId, animatePaperBall); onScopeDispose(() => { timers.forEach((timer) => window.clearTimeout(timer)); timers.clear(); }); return { activePaperBalls, hitPlayerIds, throwingAtPlayerId, throwPaperBall, }; }