@@ -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 >