599 lines
26 KiB
JavaScript

import React, {useEffect, useReducer, useRef, useState} from "react";
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
import {DrawGraph} from "../../result/DrawGraph.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
import {
CatList,
getCatName, getShieldSize,
getShieldTypeName, getSwordSize,
getSwordTypeName,
getToastMessage, timePrint,
virtual_end,
virtualScore,
win_end
} from "../../../utils/Tools.js";
import "./CMTMatchPanel.css"
import {useOBS} from "../../../hooks/useOBS.jsx";
import {useTranslation} from "react-i18next";
import {hasEffectCard, useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {ScorePanel} from "./ScoreAndCardPanel.jsx";
import {toast} from "react-toastify";
import {createPortal} from "react-dom";
import ProtectionSelector from "../../../components/ProtectionSelector.jsx";
function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171891.png"
alt=""/>
}
function CupImg2() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171892.png"
alt=""/>
}
export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS();
const {connected, setText} = useOBS();
const {t} = useTranslation("cm");
useEffect(() => {
const categoryListener = ({data}) => {
setCats([...cats.filter(c => c.id !== data.id), data])
}
const sendAddCategory = ({data}) => {
setCats([...cats, data])
}
const sendDelCategory = ({data}) => {
if (catId === data)
setCatId(-1);
setCats([...cats.filter(c => c.id !== data)])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}})
dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}})
return () => {
dispatch({type: 'removeListener', payload: categoryListener})
dispatch({type: 'removeListener', payload: sendAddCategory})
dispatch({type: 'removeListener', payload: sendDelCategory})
}
}, [cats]);
const cat = cats?.find(c => c.id === catId);
useEffect(() => {
setText("poule", cat ? cat.name : "");
}, [cat, connected]);
return <>
<div className="input-group">
<h6 style={{margin: "auto 0.5em auto 0"}}>{t('catégorie')}</h6>
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
{cats && <option value={-1}></option>}
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))}
</select>
</div>
{catId !== -1 && <CMTMatchPanel catId={catId} cat={cat} menuActions={menuActions}/>}
</>
}
function CMTMatchPanel({catId, cat, menuActions}) {
const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS();
const [trees, setTrees] = useState({raw: [], formatted: []});
const [matches, reducer] = useReducer(MarchReducer, []);
const combDispatch = useCombsDispatch();
const cardDispatch = useCardsDispatch();
function readAndConvertMatch(matches, data, combsToAdd) {
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
if (data.c1)
combsToAdd.push(data.c1)
if (data.c2)
combsToAdd.push(data.c2)
}
useEffect(() => {
if (!catId)
return;
setLoading(1);
sendRequest('getFullCategory', catId)
.then((data) => {
setTrees({
raw: data.trees.sort((a, b) => a.level - b.level),
formatted: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
})
cardDispatch({type: 'SET_ALL', payload: data.cards});
let matches2 = [];
let combsToAdd = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_ALL', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
}).finally(() => setLoading(0))
const treeListener = ({data}) => {
if (data.length < 1 || data[0].categorie !== catId)
return
setTrees({
raw: data.sort((a, b) => a.level - b.level),
formatted: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true))
})
let matches2 = [];
let combsToAdd = [];
data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_TREE', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
}
const matchListener = ({data: datas}) => {
for (const data of datas) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}})
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}})
}
}
const matchOrder = ({data}) => {
reducer({type: 'REORDER', payload: data})
}
const deleteMatch = ({data: datas}) => {
for (const data of datas)
reducer({type: 'REMOVE', payload: data})
}
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}})
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
return () => {
dispatch({type: 'removeListener', payload: treeListener})
dispatch({type: 'removeListener', payload: matchListener})
dispatch({type: 'removeListener', payload: matchOrder})
dispatch({type: 'removeListener', payload: deleteMatch})
}
}, [catId]);
return <>
<ListMatch cat={cat} matches={matches} trees={trees} menuActions={menuActions}/>
<SetTimeToChrono cat={cat} matches={matches} menuActions={menuActions}/>
</>
}
function SetTimeToChrono({cat, matches, menuActions}) {
const [catAverage, setCatAverage] = useState("---");
const [genreAverage, setGenreAverage] = useState("H");
const [nbComb, setNbComb] = useState(0);
const [preset, setPreset] = useState(undefined);
const [time, setTime] = useState({round: 0, pause: 0});
const {cards_v} = useCards();
const {t} = useTranslation("cm");
const {getComb} = useCombs();
useEffect(() => {
if (!cat || matches.filter(m => m.categorie === cat.id).length === 0) {
setCatAverage("---");
setNbComb(0);
return;
}
setPreset(cat.preset);
const genres = [];
const cats = [];
const combs = [];
for (const m of matches.filter(m => m.categorie === cat.id)) {
if (m.c1 && !combs.includes(m.c1))
combs.push(m.c1);
if (m.c2 && !combs.includes(m.c2))
combs.push(m.c2);
}
setNbComb(combs.length);
combs.map(cId => getComb(cId, null)).filter(c => c && c.categorie)
.forEach(c => {
cats.push(Math.min(CatList.length, CatList.indexOf(c.categorie) + c.overCategory))
genres.push(c.genre)
});
const catAvg = Math.round(cats.reduce((a, b) => a + b, 0) / cats.length);
setCatAverage(CatList.at(catAvg) || "---");
const genreAvg = Math.round(genres.reduce((a, b) => a + (b === "F" ? 1 : 0), 0) / genres.length);
setGenreAverage(genreAvg > 0.5 ? "F" : "H");
if (!cat.preset || !cat.preset.categories)
return;
const catAvailable = cat.preset.categories.map(c => CatList.indexOf(c.categorie));
let p;
if (catAvailable.includes(catAvg)) {
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === catAvg);
} else {
const closest = catAvailable.reduce((a, b) => Math.abs(b - catAvg) < Math.abs(a - catAvg) ? b : a);
p = cat.preset.categories.find(c => CatList.indexOf(c.categorie) === closest);
}
menuActions.current.setTimerConfig(p.roundDuration, p.pauseDuration)
setTime({round: p.roundDuration, pause: p.pauseDuration})
}, [cat, matches]);
const marches2 = matches.filter(m => m.categorie === cat.id)
.map(m => ({...m, end: m.end || virtual_end(m, cards_v)}))
return createPortal(<div className="card mb-3">
<div className="card-header">{t('informationCatégorie')}</div>
<div className="card-body">
<div className="row">
<div className="col text-start">
<div>{t('catégorie')} : {getCatName(catAverage)}</div>
<div>{t('arme', {ns: 'common'})} : {getSwordTypeName(preset?.sword)} - {t('taille')} {getSwordSize(preset?.sword, catAverage, genreAverage)}</div>
<div>{t('bouclier', {ns: 'common'})} : {getShieldTypeName(preset?.shield)} - {t('taille')} {getShieldSize(preset?.shield, catAverage)}</div>
<div>{t('duréeRound')} : {timePrint(time.round)}</div>
<div>{t('duréePause')} : {timePrint(time.pause)}</div>
<div>{t('matchTerminé')}: {marches2.filter(m => m.end).length} sur {marches2.length}</div>
<div>{t('nombreDeCombattants')} : {nbComb}</div>
</div>
<div className="col text-center">
<h6>{t('protectionObligatoire', {ns: 'common'})} :</h6>
<ProtectionSelector shield={preset?.shield !== "NONE"}
mandatoryProtection={CatList.indexOf(catAverage) <= CatList.indexOf("JUNIOR") ?
preset?.mandatoryProtection1 : preset?.mandatoryProtection2} setMandatoryProtection={() => {
}}/>
</div>
</div>
</div>
</div>, document.getElementById("infoCategory"))
}
function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1);
const {sendRequest} = useWS();
const {t} = useTranslation("cm");
useEffect(() => {
if (!cat)
return;
if ((cat.type & type) === 0)
setType(cat.type);
}, [cat]);
const handleCreatClassement = () => {
toast.promise(sendRequest("createClassementMatchs", cat.id), getToastMessage("toast.matchs.classement.create", "cm"))
}
if (!cat)
return <></>;
return <div style={{marginTop: "1em"}}>
{cat && cat.type === 3 && <>
<ul className="nav nav-tabs">
<li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>{t('poule')}
</div>
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>{(cat.treeAreClassement ? t('classement') : t('tournois'))}
</div>
</li>
</ul>
</>
}
{type === 1 && <>
<MatchList matches={matches} cat={cat} menuActions={menuActions}/>
</>}
{type === 2 && <>
{cat.treeAreClassement && !matches.some(m => m.categorie === cat.id && m.categorie_ord === -42 && (m.c1 !== undefined || m.c2 !== undefined)) ? <>
<button className="btn btn-primary" onClick={handleCreatClassement}>{t('créerLesMatchesDeClassement')}</button>
</> : <BuildTree treeData={trees} matches={matches} cat={cat} menuActions={menuActions}/>}
</>}
</div>
}
function MatchList({matches, cat, menuActions, classement = false, currentMatch = null, setCurrentMatch, getNext}) {
const [activeMatch, setActiveMatch] = useState(null)
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm");
const {cards_v, getHeightCardForCombInMatch} = useCards();
const {sendNotify, setState} = useWS();
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = classement
? matches.filter(m => m.categorie_ord === -42 && m.categorie === cat.id)
.map(m => ({...m, ...win_end(m, cards_v)}))
: matches.filter(m => m.categorie_ord !== -42 && m.categorie === cat.id)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, cards_v)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => {
if (classement)
return true;
return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice)
}
const match = matches.find(m => m.id === activeMatch)
useEffect(() => {
if (!match) {
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
} else {
publicAffDispatch({
type: 'SET_DATA',
payload: {
c1: match.c1,
c2: match.c2,
next: marches2.filter((m, index) => !m.end && isActiveMatch(index) && m.id !== activeMatch).map(m => ({
c1: m.c1,
c2: m.c2
}))
}
});
}
}, [match]);
useEffect(() => {
if (match && match.poule !== lice)
if (!classement)
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id)
}, [lice]);
useEffect(() => {
if (marches2.length === 0)
return;
if (marches2.some(m => m.id === (classement ? currentMatch?.matchSelect : activeMatch)))
return;
if (classement) {
getNext.current = (id) => {
if (id === null)
return marches2.findLast((m, index) => !m.end && isActiveMatch(index))?.id;
const index = marches2.findIndex(m => m.id === id);
return marches2.slice(0, index).reverse().find((m, index2) => !m.end && isActiveMatch(marches2.length - 1 - index2))?.id;
}
} else {
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
}
}, [matches])
useEffect(() => {
setState({selectedMatch: activeMatch});
sendNotify("sendSelectMatch", activeMatch);
}, [activeMatch]);
const handleMatchClick = (matchId) => {
if (classement) {
setCurrentMatch({matchSelect: matchId, matchNext: marches2.reverse().find(m => !m.end && m.id !== matchId)?.id});
} else {
setActiveMatch(matchId);
}
}
const GetCard = ({combId, match, cat}) => {
const c = getHeightCardForCombInMatch(combId, match)
if (!c)
return <></>
let bg = "";
switch (c.type) {
case "YELLOW":
bg = " bg-warning";
break;
case "RED":
bg = " bg-danger";
break;
case "BLACK":
bg = " bg-dark text-white";
break;
case "BLUE":
bg = " bg-primary text-white";
break;
}
return <span
className={"position-absolute top-0 start-100 translate-middle-y badge border border-light p-2" + bg +
(c.match === match.id ? " rounded-circle" : (hasEffectCard(c, match.id, cat.id) ? "" : " bg-opacity-50"))}>
<span className="visually-hidden">card</span></span>
}
return <>
{liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "15em", marginTop: "0.5em"}}>
<label className="input-group-text" htmlFor="selectLice">{t('zoneDeCombat')}</label>
<select className="form-select" id="selectLice" value={lice} onChange={e => {
setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value);
}}>
{liceName.map((l, index) => (
<option key={index} value={l}>{l}</option>
))}
</select>
</div>
}
<div className="table-responsive-xxl overflow-y-auto" style={{maxHeight: "50vh"}}>
<table className="table table-striped table-hover">
<thead>
<tr>
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">Z</th>}
{!classement && <th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>}
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">{t('no')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">{t('rouge')}</th>
<th style={{textAlign: "center"}} scope="col">{t('blue')}</th>
<th style={{textAlign: "center"}} scope="col"></th>
</tr>
</thead>
<tbody className="table-group-divider">
{marches2.map((m, index) => (
<tr key={m.id}
className={m.id === (classement ? currentMatch?.matchSelect : activeMatch) ? "table-primary" : (m.end ? "table-success" : (isActiveMatch(index) ? "" : "table-warning"))}
onClick={() => handleMatchClick(m.id)}>
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td>}
{!classement && <td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>}
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{index >= firstIndex ? index + 1 - firstIndex : ""}</th>
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && ((m.win > 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
<small className="position-relative"><CombName combId={m.c1}/>
<GetCard match={m} combId={m.c1} cat={cat}/></small></td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
<small className="position-relative"><CombName combId={m.c2}/>
<GetCard match={m} combId={m.c2} cat={cat}/></small></td>
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && ((m.win < 0 && <CupImg/>) || (m.win === 0 && <CupImg2/>))}</td>
</tr>
))}
</tbody>
</table>
</div>
{activeMatch && !classement &&
<LoadingProvider><ScorePanel matchId={activeMatch} matchs={matches} match={match} menuActions={menuActions}/></LoadingProvider>}
</>
}
function BuildTree({treeData, matches, cat, menuActions}) {
const scrollRef = useRef(null)
const [currentMatch, setCurrentMatch] = useState(null)
const {getComb} = useCombs()
const publicAffDispatch = usePubAffDispatch();
const {cards_v} = useCards();
const {sendNotify, setState} = useWS();
const getNext = useRef(null);
const rtrees = useRef(null);
const match = matches.find(m => m.id === currentMatch?.matchSelect)
useEffect(() => {
if (!match) {
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined}});
} else {
publicAffDispatch({type: 'SET_DATA', payload: {c1: match.c1, c2: match.c2}});
}
}, [match]);
const next_match = matches.find(m => m.id === currentMatch?.matchNext)
useEffect(() => {
if (!next_match) {
publicAffDispatch({type: 'SET_DATA', payload: {next: []}});
} else {
publicAffDispatch({type: 'SET_DATA', payload: {next: [{c1: next_match.c1, c2: next_match.c2}]}});
}
}, [next_match]);
function parseTree(data_in, matches_) {
if (data_in?.data == null)
return null
const matchData = matches_.find(m => m.id === data_in.data)
const c1 = getComb(matchData?.c1)
const c2 = getComb(matchData?.c2)
const scores2 = []
for (const score of matchData?.scores) {
scores2.push({
...score,
s1: virtualScore(matchData?.c1, score, matchData, cards_v),
s2: virtualScore(matchData?.c2, score, matchData, cards_v)
})
}
let node = new TreeNode({
...matchData,
...win_end(matchData, cards_v),
scores: scores2,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left, matches_)
node.right = parseTree(data_in?.right, matches_)
return node
}
function initTree(data_in, matches_) {
let out = []
let out2 = []
for (let i = 0; i < data_in.raw.length; i++) {
if (data_in.raw.at(i).level > -10) {
out.push(parseTree(data_in.formatted.at(i), matches_))
out2.push(parseTree(data_in.formatted.at(i), matches_))
}
}
return [out, out2.reverse()]
}
const [trees, rTrees] = initTree(treeData, matches);
rtrees.current = rTrees;
useEffect(() => {
if (matches.length === 0)
return;
if (matches.some(m => m.id === currentMatch?.matchSelect))
return;
const timeout = setTimeout(() => {
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
const matchId = ((getNext.current) ? getNext.current(null) : null) || new TreeNode(null).nextMatchTree(rTrees_);
const next = matchId ? ((getNext.current) ? getNext.current(matchId) : null) || new TreeNode(matchId).nextMatchTree(rTrees_) : null;
setCurrentMatch({matchSelect: matchId, matchNext: next});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}, 200);
return () => clearTimeout(timeout);
}, [matches])
const setCurrentMatch_ = (o) => {
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
const matchId = o.matchSelect ? o.matchSelect : new TreeNode(null).nextMatchTree(rTrees_);
const next = o.matchNext ? o.matchNext : new TreeNode(matchId).nextMatchTree(rTrees_);
setCurrentMatch({matchSelect: matchId, matchNext: next});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}
const onMatchClick = (rect, matchId, __) => {
const rTrees_ = rtrees.current ? rtrees.current : rTrees;
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(rTrees_)});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}
const onClickVoid = () => {
}
return <div>
<div className="overflow-y-auto" style={{maxHeight: "50vh"}}>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23} cards={cards_v}/>
{cat.fullClassement &&
<MatchList matches={treeData.raw.filter(n => n.level <= -10).reverse().map(d => matches.find(m => m.id === d.match?.id))}
cat={cat} menuActions={menuActions} classement={true} currentMatch={currentMatch} setCurrentMatch={setCurrentMatch_}
getNext={getNext}/>}
</div>
</div>
{currentMatch?.matchSelect &&
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matchs={matches} match={match}
menuActions={menuActions}/></LoadingProvider>}
</div>
}