Files
CRAPP/src/lib/scout-store.ts
T

179 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-08-06 08:36:47 +02:00
import { useSyncExternalStore } from "react";
import { giocatori, classifica, type Giocatore, type RigaClassifica } from "./crapp-data";
export type AzioneTipo =
| "attacco"
| "ace"
| "muro"
| "errore"
| "punto_avv"
| "errore_avv";
export const azioniMeta: Record<
AzioneTipo,
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
> = {
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" },
};
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,
};
});
}
/** Classifica demo aggiornata con i match scoutati (solo la nostra riga). */
export function classificaConScout(matches: ScoutMatch[]): RigaClassifica[] {
if (matches.length === 0) return classifica;
const agg = matches.reduce(
(s, m) => {
const vinta = m.setNostri > m.setLoro;
s.giocate += 1;
s.vinte += vinta ? 1 : 0;
s.perse += vinta ? 0 : 1;
s.setFatti += m.setNostri;
s.setSubiti += m.setLoro;
s.punti += vinta ? (m.setLoro <= 1 ? 3 : 2) : m.setLoro === 3 && m.setNostri === 2 ? 1 : 0;
return s;
},
{ giocate: 0, vinte: 0, perse: 0, setFatti: 0, setSubiti: 0, punti: 0 },
);
return classifica
.map((r) =>
r.squadra === "CRAP Volley"
? {
...r,
giocate: r.giocate + agg.giocate,
vinte: r.vinte + agg.vinte,
perse: r.perse + agg.perse,
setFatti: r.setFatti + agg.setFatti,
setSubiti: r.setSubiti + agg.setSubiti,
punti: r.punti + agg.punti,
}
: r,
)
.sort((a, b) => b.punti - a.punti || b.setFatti - b.setSubiti - (a.setFatti - a.setSubiti))
.map((r, i) => ({ ...r, pos: i + 1 }));
}