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 { Dices, Trash2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
@@ -36,6 +36,15 @@ export function CreateMatchPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Анимация-«барабан» рандома: для крутящейся строки — текущее мелькающее название.
|
// Анимация-«барабан» рандома: для крутящейся строки — текущее мелькающее название.
|
||||||
const [spin, setSpin] = useState<Record<number, string | 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 (lm || lf) return <Spinner />;
|
||||||
if (!groupId) return <div className="muted">Нет активной группы.</div>;
|
if (!groupId) return <div className="muted">Нет активной группы.</div>;
|
||||||
@@ -52,39 +61,72 @@ export function CreateMatchPage() {
|
|||||||
const addRow = () => setRows((rs) => [...rs, emptyDraft()]);
|
const addRow = () => setRows((rs) => [...rs, emptyDraft()]);
|
||||||
const removeRow = (idx: number) => setRows((rs) => rs.filter((_, i) => i !== idx));
|
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 randomizeRow = async (idx: number) => {
|
||||||
const pool = factions ?? [];
|
const pool = factions ?? [];
|
||||||
if (pool.length === 0) return;
|
if (pool.length === 0 || spin[idx] != null) return;
|
||||||
// Запускаем «барабан»: быстро мелькаем названиями фракций.
|
|
||||||
const ticker = setInterval(() => {
|
|
||||||
const name = pool[Math.floor(Math.random() * pool.length)].name_ru;
|
|
||||||
setSpin((s) => ({ ...s, [idx]: name }));
|
|
||||||
}, 60);
|
|
||||||
try {
|
try {
|
||||||
const faction = await randomize.mutateAsync({
|
const faction = await randomize.mutateAsync({
|
||||||
group_id: groupId,
|
group_id: groupId,
|
||||||
exclude_faction_ids: takenFactionIds(idx),
|
exclude_faction_ids: takenFactionIds(idx),
|
||||||
});
|
});
|
||||||
await new Promise((r) => setTimeout(r, 700)); // даём барабану покрутиться
|
await animateReel(idx, pool.map((f) => f.name_ru), faction.name_ru, () =>
|
||||||
clearInterval(ticker);
|
update(idx, { faction_id: faction.id, was_random: true }),
|
||||||
setSpin((s) => ({ ...s, [idx]: null }));
|
);
|
||||||
update(idx, { faction_id: faction.id, was_random: true });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearInterval(ticker);
|
if (reelTimers.current[idx]) clearTimeout(reelTimers.current[idx]);
|
||||||
setSpin((s) => ({ ...s, [idx]: null }));
|
setSpin((s) => ({ ...s, [idx]: null }));
|
||||||
toast.show(e instanceof ApiError ? e.message : "Ошибка рандома");
|
toast.show(e instanceof ApiError ? e.message : "Ошибка рандома");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const randomizeAll = () => {
|
const randomizeAll = async () => {
|
||||||
const pool = [...(factions ?? [])];
|
const all = factions ?? [];
|
||||||
|
if (all.length === 0) return;
|
||||||
|
const pool = [...all];
|
||||||
for (let i = pool.length - 1; i > 0; i--) {
|
for (let i = pool.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[pool[i], pool[j]] = [pool[j], pool[i]];
|
[pool[i], pool[j]] = [pool[j], pool[i]];
|
||||||
}
|
}
|
||||||
if (pool.length < rows.length) toast.show("Фракций меньше, чем игроков — возможны повторы.");
|
if (pool.length < rows.length) toast.show("Фракций меньше, чем игроков — возможны повторы.");
|
||||||
setRows((rs) =>
|
const names = all.map((f) => f.name_ru);
|
||||||
rs.map((r, i) => ({ ...r, faction_id: pool[i % pool.length]?.id ?? "", was_random: true })),
|
// Барабан крутится во всех строках одновременно, каждая садится на свою фракцию.
|
||||||
|
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 memberOptions = (members ?? []) as MemberRead[];
|
||||||
|
const spinning = Object.values(spin).some((v) => v != null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h3>Этап 1 — старт партии</h3>
|
<h3>Этап 1 — старт партии</h3>
|
||||||
<p className="muted small">Выберите игроков и фракции. Места и итоги укажете при завершении.</p>
|
<p className="muted small">Выберите игроков и фракции. Места и итоги укажете при завершении.</p>
|
||||||
<button className="btn" onClick={randomizeAll}>
|
<button className="btn" onClick={randomizeAll} disabled={spinning}>
|
||||||
<Dices size={18} /> Рандом всем
|
<Dices size={18} /> Рандом всем
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user