UI: рандом фракции — «Рандом всем» тоже анимируется; плавное замедление барабана и пауза 0.5с перед выбором
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(null);
|
||||
// Анимация-«барабан» рандома: для крутящейся строки — текущее мелькающее название.
|
||||
const [spin, setSpin] = useState<Record<number, string | null>>({});
|
||||
const reelTimers = useRef<Record<number, ReturnType<typeof setTimeout>>>({});
|
||||
|
||||
// Снимаем таймеры барабана при размонтировании.
|
||||
useEffect(
|
||||
() => () => {
|
||||
Object.values(reelTimers.current).forEach((t) => clearTimeout(t));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (lm || lf) return <Spinner />;
|
||||
if (!groupId) return <div className="muted">Нет активной группы.</div>;
|
||||
@@ -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<void> =>
|
||||
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 (
|
||||
<div>
|
||||
<div className="card">
|
||||
<h3>Этап 1 — старт партии</h3>
|
||||
<p className="muted small">Выберите игроков и фракции. Места и итоги укажете при завершении.</p>
|
||||
<button className="btn" onClick={randomizeAll}>
|
||||
<button className="btn" onClick={randomizeAll} disabled={spinning}>
|
||||
<Dices size={18} /> Рандом всем
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user