diff --git a/frontend/src/pages/CreateMatchPage.tsx b/frontend/src/pages/CreateMatchPage.tsx index ff7de5f..5e005c6 100644 --- a/frontend/src/pages/CreateMatchPage.tsx +++ b/frontend/src/pages/CreateMatchPage.tsx @@ -1,5 +1,5 @@ import { Dices, Trash2 } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { ApiError } from "../api/client"; @@ -36,6 +36,15 @@ export function CreateMatchPage() { const [error, setError] = useState(null); // Анимация-«барабан» рандома: для крутящейся строки — текущее мелькающее название. const [spin, setSpin] = useState>({}); + const reelTimers = useRef>>({}); + + // Снимаем таймеры барабана при размонтировании. + useEffect( + () => () => { + Object.values(reelTimers.current).forEach((t) => clearTimeout(t)); + }, + [], + ); if (lm || lf) return ; if (!groupId) return
Нет активной группы.
; @@ -52,39 +61,72 @@ export function CreateMatchPage() { const addRow = () => setRows((rs) => [...rs, emptyDraft()]); const removeRow = (idx: number) => setRows((rs) => rs.filter((_, i) => i !== idx)); + // «Барабан»: мелькаем названиями с нарастающим интервалом (плавное замедление), + // затем рулетка останавливается на выбранной фракции, держит её 0.5с — и только + // после этого фракция «выбирается» (onReveal: показываем select с результатом). + const animateReel = ( + idx: number, + names: string[], + finalName: string, + onReveal: () => void, + ): Promise => + new Promise((resolve) => { + let delay = 50; + const tick = () => { + setSpin((s) => ({ ...s, [idx]: names[Math.floor(Math.random() * names.length)] })); + delay *= 1.18; + if (delay < 300) { + reelTimers.current[idx] = setTimeout(tick, delay); + } else { + reelTimers.current[idx] = setTimeout(() => { + setSpin((s) => ({ ...s, [idx]: finalName })); // остановка на выбранной + reelTimers.current[idx] = setTimeout(() => { + onReveal(); + setSpin((s) => ({ ...s, [idx]: null })); + resolve(); + }, 500); // пауза перед «выбором» + }, delay); + } + }; + tick(); + }); + const randomizeRow = async (idx: number) => { const pool = factions ?? []; - if (pool.length === 0) return; - // Запускаем «барабан»: быстро мелькаем названиями фракций. - const ticker = setInterval(() => { - const name = pool[Math.floor(Math.random() * pool.length)].name_ru; - setSpin((s) => ({ ...s, [idx]: name })); - }, 60); + if (pool.length === 0 || spin[idx] != null) return; try { const faction = await randomize.mutateAsync({ group_id: groupId, exclude_faction_ids: takenFactionIds(idx), }); - await new Promise((r) => setTimeout(r, 700)); // даём барабану покрутиться - clearInterval(ticker); - setSpin((s) => ({ ...s, [idx]: null })); - update(idx, { faction_id: faction.id, was_random: true }); + await animateReel(idx, pool.map((f) => f.name_ru), faction.name_ru, () => + update(idx, { faction_id: faction.id, was_random: true }), + ); } catch (e) { - clearInterval(ticker); + if (reelTimers.current[idx]) clearTimeout(reelTimers.current[idx]); setSpin((s) => ({ ...s, [idx]: null })); toast.show(e instanceof ApiError ? e.message : "Ошибка рандома"); } }; - const randomizeAll = () => { - const pool = [...(factions ?? [])]; + const randomizeAll = async () => { + const all = factions ?? []; + if (all.length === 0) return; + const pool = [...all]; for (let i = pool.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } if (pool.length < rows.length) toast.show("Фракций меньше, чем игроков — возможны повторы."); - setRows((rs) => - rs.map((r, i) => ({ ...r, faction_id: pool[i % pool.length]?.id ?? "", was_random: true })), + const names = all.map((f) => f.name_ru); + // Барабан крутится во всех строках одновременно, каждая садится на свою фракцию. + await Promise.all( + rows.map((_, idx) => { + const f = pool[idx % pool.length]; + return animateReel(idx, names, f.name_ru, () => + update(idx, { faction_id: f.id, was_random: true }), + ); + }), ); }; @@ -116,13 +158,14 @@ export function CreateMatchPage() { }; const memberOptions = (members ?? []) as MemberRead[]; + const spinning = Object.values(spin).some((v) => v != null); return (

Этап 1 — старт партии

Выберите игроков и фракции. Места и итоги укажете при завершении.

-