Compare commits
2
Commits
cec3aa25a6
...
75feb9d15a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75feb9d15a | ||
|
|
0745d07887 |
@@ -31,9 +31,14 @@ export function BottomBar({ onMenu }: { onMenu: () => void }) {
|
||||
holdTimer.current = null;
|
||||
}
|
||||
};
|
||||
const endHold = () => {
|
||||
clearHold();
|
||||
if (!holdFired.current) onMenu(); // обычный тап
|
||||
// Открытие меню — на нативном onClick (надёжно на мобильных, в отличие от pointerup,
|
||||
// который не приходит при pointercancel). Долгий зажим подавляет этот клик.
|
||||
const onMenuClick = () => {
|
||||
if (holdFired.current) {
|
||||
holdFired.current = false;
|
||||
return;
|
||||
}
|
||||
onMenu();
|
||||
};
|
||||
|
||||
const onCreate = () => {
|
||||
@@ -57,8 +62,9 @@ export function BottomBar({ onMenu }: { onMenu: () => void }) {
|
||||
<button
|
||||
className="slot"
|
||||
aria-label="Меню"
|
||||
onClick={onMenuClick}
|
||||
onPointerDown={startHold}
|
||||
onPointerUp={endHold}
|
||||
onPointerUp={clearHold}
|
||||
onPointerLeave={clearHold}
|
||||
onPointerCancel={clearHold}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
touch-action: manipulation; /* без задержки/отмены тапа на мобильных */
|
||||
}
|
||||
.bottom-bar .slot .name { color: var(--text); font-weight: 600; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.bottom-bar .spacer { width: var(--fab-width); flex: 0 0 var(--fab-width); }
|
||||
|
||||
Reference in New Issue
Block a user