// Rôle : gère l'onglet équipes équilibrées du Randomizer et ses exclusions. import { useEffect, useRef, useState } from "react"; import { CompactDropdown } from "../../../../components/CompactDropdown.jsx"; import { Icon } from "../../../../components/Icon.jsx"; import { useDraftForm } from "../../../../hooks/useDraftForm.js"; import { RANDOMIZER_LIMITS, addRandomizerHistory, clampNumber, createBalancedTeams, filterExclusionsForPlayers } from "../randomizerUtils.js"; import { RandomizerHistory } from "./RandomizerShared.jsx"; import { DEFAULT_EXCLUSION_DRAFT, TEAM_REEL_DELAYS, hasReducedMotion, pickRandomItems } from "./randomizerUiUtils.js"; export function TeamsRandomizer({ data, textContent, onChange, context }) { const exclusionDraft = useDraftForm(DEFAULT_EXCLUSION_DRAFT); const [animating, setAnimating] = useState(false); const [animationTeams, setAnimationTeams] = useState([]); const [reelPlayers, setReelPlayers] = useState([]); const [reelIndex, setReelIndex] = useState(0); const [reelDuration, setReelDuration] = useState(TEAM_REEL_DELAYS[0]); const [namesText, setNamesText] = useState(""); const timersRef = useRef([]); const activePlayers = data.players.filter((player) => player.active); const storedSelectionCount = clampNumber(data.playerSelectionCount, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, data.teamCount); const [selectionCountDraft, setSelectionCountDraft] = useState(String(storedSelectionCount)); const selectionCount = clampNumber(selectionCountDraft, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, storedSelectionCount); const displayedTeams = animating ? animationTeams : data.teams; const exclusionMode = exclusionDraft.values.mode === "source" ? "source" : "group"; useEffect(() => { setSelectionCountDraft(String(storedSelectionCount)); }, [storedSelectionCount]); useEffect(() => () => { timersRef.current.forEach((timerId) => window.clearTimeout(timerId)); timersRef.current = []; }, []); function queueTimer(callback, delay) { const timerId = window.setTimeout(callback, delay); timersRef.current.push(timerId); return timerId; } function clearTeamTimers() { timersRef.current.forEach((timerId) => window.clearTimeout(timerId)); timersRef.current = []; } function commitSelectionCount(value = selectionCountDraft) { const nextSelectionCount = clampNumber(value, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, storedSelectionCount); setSelectionCountDraft(String(nextSelectionCount)); onChange({ ...data, playerSelectionCount: nextSelectionCount }); } function importPlayers(event) { event.preventDefault(); const availableSlots = RANDOMIZER_LIMITS.teamPlayersMax - data.players.length; const names = namesText .split(/\r?\n/) .map((name) => name.trim()) .filter(Boolean) .slice(0, availableSlots); if (!names.length) return; onChange({ ...data, players: [ ...data.players, ...names.map((name) => ({ id: context.uid("randomizer"), name, score: 0, active: true })) ] }); setNamesText(""); } function updatePlayer(playerId, patch) { onChange({ ...data, players: data.players.map((player) => player.id === playerId ? { ...player, ...patch } : player) }); } function removePlayer(playerId) { onChange({ ...data, players: data.players.filter((player) => player.id !== playerId), exclusions: data.exclusions .filter((exclusion) => exclusion.playerAId !== playerId) .map((exclusion) => ({ ...exclusion, playerBIds: (exclusion.playerBIds || []).filter((targetId) => targetId !== playerId) })) .filter((exclusion) => exclusion.playerBIds.length) }); } function clearPlayers() { onChange({ ...data, players: [], exclusions: [], teams: [] }); } function toggleExclusionTarget(playerId) { const currentIds = exclusionDraft.values.playerBIds || []; exclusionDraft.setField( "playerBIds", currentIds.includes(playerId) ? currentIds.filter((targetId) => targetId !== playerId) : [...currentIds, playerId] ); } function addExclusion(event) { exclusionDraft.handleSubmit(event, (draft, { reset }) => { const mode = draft.mode === "source" ? "source" : "group"; const selectedIds = [...new Set(Array.isArray(draft.playerBIds) ? draft.playerBIds.filter(Boolean) : [])]; const playerAId = mode === "group" ? selectedIds[0] : draft.playerAId; const playerBIds = mode === "group" ? selectedIds.slice(1) : selectedIds.filter((playerId) => playerId !== draft.playerAId); if (!playerAId || !playerBIds.length) return; const exists = data.exclusions.some((exclusion) => exclusion.playerAId === playerAId && (exclusion.mode || "source") === mode && [...(exclusion.playerBIds || [])].sort().join(":") === [...playerBIds].sort().join(":")); if (exists) return; onChange({ ...data, exclusions: [...data.exclusions, { id: context.uid("randomizer"), playerAId, playerBIds, mode }] }); reset(); }); } function launchTeams() { clearTeamTimers(); const selectedPlayers = data.playerSelectionMode === "count" ? pickRandomItems(activePlayers, Math.min(selectionCount, activePlayers.length)) : activePlayers; const selectedExclusions = filterExclusionsForPlayers(data.exclusions, selectedPlayers); const result = createBalancedTeams(selectedPlayers, data.teamCount, selectedExclusions); if (!result.ok) { context.notify?.(result.error === "not_enough_players" ? textContent.notEnoughPlayers || "Pas assez de joueurs actifs." : textContent.constraintsImpossible || "Contraintes impossibles à satisfaire."); return; } const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), teams: result.teams }; if (hasReducedMotion()) { onChange({ ...data, teams: result.teams, history: addRandomizerHistory(data.history, historyEntry) }); return; } const emptyTeams = result.teams.map((team) => ({ ...team, score: 0, players: [] })); const assignments = result.teams.flatMap((team) => team.players.map((player) => ({ teamId: team.id, player }))); setAnimating(true); setAnimationTeams(emptyTeams); setReelPlayers(selectedPlayers); setReelIndex(0); setReelDuration(TEAM_REEL_DELAYS[0]); function animateAssignment(index) { const assignment = assignments[index]; if (!assignment) { setAnimating(false); setReelPlayers([]); setReelIndex(0); onChange({ ...data, teams: result.teams, history: addRandomizerHistory(data.history, historyEntry) }); return; } let ticks = 0; const tick = () => { ticks += 1; const isFinalTick = ticks >= TEAM_REEL_DELAYS.length; const tickDelay = TEAM_REEL_DELAYS[Math.min(ticks, TEAM_REEL_DELAYS.length - 1)]; const nextIndex = isFinalTick ? Math.max(0, selectedPlayers.findIndex((player) => player.id === assignment.player.id)) : Math.floor(Math.random() * selectedPlayers.length); setReelIndex(nextIndex); setReelDuration(Math.max(54, Math.round(tickDelay * 0.86))); if (!isFinalTick) { queueTimer(tick, tickDelay); return; } queueTimer(() => { setAnimationTeams((currentTeams) => currentTeams.map((team) => { if (team.id !== assignment.teamId) return team; return { ...team, score: team.score + assignment.player.score, players: [...team.players, { ...assignment.player, arriving: true }] }; })); queueTimer(() => animateAssignment(index + 1), 260); }, 220); }; tick(); } animateAssignment(0); } function clearTeamsHistory() { onChange({ ...data, history: [] }); } function removeTeamsHistoryEntry(entryId) { onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) }); } return (
{reelPlayers.length ? ( <> {reelPlayers[reelIndex]?.name || ""} ) : }
{displayedTeams.length ? displayedTeams.map((team) => (

{team.name}

{textContent.teamScoreLabel || "Score"} {team.score} {team.players.map((player) => {player.name})}
)) :

{textContent.emptyTeams ?? ""}

}