2026-08-06 08:36:47 +02:00
|
|
|
import { useSyncExternalStore } from "react";
|
2026-09-01 13:38:33 +02:00
|
|
|
import { giocatori, type Giocatore } from "./crapp-data";
|
2026-08-06 08:36:47 +02:00
|
|
|
|
2026-09-01 13:38:33 +02:00
|
|
|
export type AzioneTipo = "attacco" | "ace" | "muro" | "errore" | "punto_avv" | "errore_avv";
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
|
|
|
export const azioniMeta: Record<
|
|
|
|
|
AzioneTipo,
|
|
|
|
|
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
|
|
|
|
|
> = {
|
2026-09-01 13:38:33 +02:00
|
|
|
attacco: {
|
|
|
|
|
label: "Punto attacco",
|
|
|
|
|
short: "Punto",
|
|
|
|
|
nostro: true,
|
|
|
|
|
richiedeGiocatore: true,
|
|
|
|
|
className: "bg-accent text-accent-foreground",
|
|
|
|
|
},
|
|
|
|
|
ace: {
|
|
|
|
|
label: "Ace",
|
|
|
|
|
short: "Ace",
|
|
|
|
|
nostro: true,
|
|
|
|
|
richiedeGiocatore: true,
|
|
|
|
|
className: "bg-success text-success-foreground",
|
|
|
|
|
},
|
|
|
|
|
muro: {
|
|
|
|
|
label: "Muro",
|
|
|
|
|
short: "Muro",
|
|
|
|
|
nostro: true,
|
|
|
|
|
richiedeGiocatore: true,
|
|
|
|
|
className: "bg-info text-info-foreground",
|
|
|
|
|
},
|
|
|
|
|
errore: {
|
|
|
|
|
label: "Errore nostro",
|
|
|
|
|
short: "Errore",
|
|
|
|
|
nostro: false,
|
|
|
|
|
richiedeGiocatore: true,
|
|
|
|
|
className: "bg-destructive text-destructive-foreground",
|
|
|
|
|
},
|
|
|
|
|
punto_avv: {
|
|
|
|
|
label: "Punto avversario",
|
|
|
|
|
short: "Punto avv.",
|
|
|
|
|
nostro: false,
|
|
|
|
|
richiedeGiocatore: false,
|
|
|
|
|
className: "bg-muted text-muted-foreground",
|
|
|
|
|
},
|
|
|
|
|
errore_avv: {
|
|
|
|
|
label: "Errore avversario",
|
|
|
|
|
short: "Err. avv.",
|
|
|
|
|
nostro: true,
|
|
|
|
|
richiedeGiocatore: false,
|
|
|
|
|
className: "bg-secondary text-foreground",
|
|
|
|
|
},
|
2026-08-06 08:36:47 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type Azione = {
|
|
|
|
|
id: string;
|
|
|
|
|
tipo: AzioneTipo;
|
|
|
|
|
giocatoreId?: string;
|
|
|
|
|
set: number;
|
|
|
|
|
ts: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ScoutMatch = {
|
|
|
|
|
id: string;
|
|
|
|
|
data: string;
|
|
|
|
|
avversario: string;
|
|
|
|
|
casa: boolean;
|
|
|
|
|
setNostri: number;
|
|
|
|
|
setLoro: number;
|
|
|
|
|
parziali: Array<[number, number]>;
|
|
|
|
|
mvp: string;
|
|
|
|
|
azioni: Azione[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const KEY = "crapp-scout-v1";
|
|
|
|
|
|
|
|
|
|
let cache: ScoutMatch[] | null = null;
|
|
|
|
|
const listeners = new Set<() => void>();
|
|
|
|
|
|
|
|
|
|
function read(): ScoutMatch[] {
|
|
|
|
|
if (cache) return cache;
|
|
|
|
|
if (typeof window === "undefined") return (cache = []);
|
|
|
|
|
try {
|
|
|
|
|
const raw = window.localStorage.getItem(KEY);
|
|
|
|
|
cache = raw ? (JSON.parse(raw) as ScoutMatch[]) : [];
|
|
|
|
|
} catch {
|
|
|
|
|
cache = [];
|
|
|
|
|
}
|
|
|
|
|
return cache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function write(next: ScoutMatch[]) {
|
|
|
|
|
cache = next;
|
|
|
|
|
try {
|
|
|
|
|
window.localStorage.setItem(KEY, JSON.stringify(next));
|
|
|
|
|
} catch {
|
|
|
|
|
/* storage non disponibile */
|
|
|
|
|
}
|
|
|
|
|
listeners.forEach((l) => l());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function salvaScoutMatch(m: ScoutMatch) {
|
|
|
|
|
write([m, ...read()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function eliminaScoutMatch(id: string) {
|
|
|
|
|
write(read().filter((m) => m.id !== id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useScoutMatches(): ScoutMatch[] {
|
|
|
|
|
return useSyncExternalStore(
|
|
|
|
|
(cb) => {
|
|
|
|
|
listeners.add(cb);
|
|
|
|
|
return () => listeners.delete(cb);
|
|
|
|
|
},
|
|
|
|
|
() => read(),
|
|
|
|
|
() => [],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Somma delle azioni di un match per giocatore. */
|
|
|
|
|
export function totaliPerGiocatore(azioni: Azione[]) {
|
|
|
|
|
const out = new Map<string, { punti: number; ace: number; muri: number; errori: number }>();
|
|
|
|
|
for (const a of azioni) {
|
|
|
|
|
if (!a.giocatoreId) continue;
|
|
|
|
|
const cur = out.get(a.giocatoreId) ?? { punti: 0, ace: 0, muri: 0, errori: 0 };
|
|
|
|
|
if (a.tipo === "attacco") cur.punti += 1;
|
|
|
|
|
if (a.tipo === "ace") {
|
|
|
|
|
cur.ace += 1;
|
|
|
|
|
cur.punti += 1;
|
|
|
|
|
}
|
|
|
|
|
if (a.tipo === "muro") {
|
|
|
|
|
cur.muri += 1;
|
|
|
|
|
cur.punti += 1;
|
|
|
|
|
}
|
|
|
|
|
if (a.tipo === "errore") cur.errori += 1;
|
|
|
|
|
out.set(a.giocatoreId, cur);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Totali di squadra ricavati dallo scout (dato tecnico, non personale). */
|
|
|
|
|
export function totaliSquadra(matches: ScoutMatch[]) {
|
|
|
|
|
const out = { punti: 0, ace: 0, muri: 0, errori: 0 };
|
|
|
|
|
for (const m of matches) {
|
|
|
|
|
for (const t of totaliPerGiocatore(m.azioni).values()) {
|
|
|
|
|
out.punti += t.punti;
|
|
|
|
|
out.ace += t.ace;
|
|
|
|
|
out.muri += t.muri;
|
|
|
|
|
out.errori += t.errori;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Presenze e MVP ricavati dalle partite scoutate: nessuna statistica individuale offensiva.
|
|
|
|
|
* `mvpPerMatch` mappa idPartita -> nome dell'MVP eletto dalla squadra. */
|
|
|
|
|
export function giocatoriConScout(
|
|
|
|
|
matches: ScoutMatch[],
|
|
|
|
|
mvpPerMatch: Record<string, string> = {},
|
|
|
|
|
): Giocatore[] {
|
|
|
|
|
return giocatori.map((g) => {
|
|
|
|
|
let presenze = 0;
|
|
|
|
|
let mvp = 0;
|
|
|
|
|
for (const m of matches) {
|
|
|
|
|
if (totaliPerGiocatore(m.azioni).has(g.id)) presenze += 1;
|
|
|
|
|
if (mvpPerMatch[m.id] === g.nome) mvp += 1;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...g,
|
|
|
|
|
mvp: g.mvp + mvp,
|
|
|
|
|
presenze: g.presenze + presenze,
|
|
|
|
|
totaliEventi: g.totaliEventi + matches.length,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|