271 lines
8.9 KiB
TypeScript
271 lines
8.9 KiB
TypeScript
import {
|
|
Trophy,
|
|
Repeat,
|
|
Lock,
|
|
Ghost,
|
|
Zap,
|
|
Rocket,
|
|
Anchor,
|
|
Stethoscope,
|
|
AlarmClock,
|
|
ClipboardCheck,
|
|
CircleDot,
|
|
Toilet,
|
|
} from "lucide-react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import type { Giocatore } from "./crapp-data";
|
|
|
|
export type Grado = "bronzo" | "argento" | "oro";
|
|
|
|
export const gradiOrdine: Grado[] = ["bronzo", "argento", "oro"];
|
|
|
|
export const gradoMeta: Record<
|
|
Grado,
|
|
{ label: string; text: string; bg: string; ring: string }
|
|
> = {
|
|
bronzo: { label: "Bronzo", text: "text-bronzo", bg: "bg-bronzo/15", ring: "ring-bronzo/40" },
|
|
argento: { label: "Argento", text: "text-argento", bg: "bg-argento/20", ring: "ring-argento/50" },
|
|
oro: { label: "Oro", text: "text-oro", bg: "bg-oro/20", ring: "ring-oro/50" },
|
|
};
|
|
|
|
export type BadgeDef = {
|
|
id: string;
|
|
nome: string;
|
|
descrizione: string;
|
|
unita: string;
|
|
icon: LucideIcon;
|
|
soglie: Record<Grado, number>;
|
|
valore: (g: Giocatore) => number;
|
|
/** Badge segreto: nome e requisiti restano nascosti finché non si sblocca. */
|
|
segreto?: boolean;
|
|
/** Frase celebrativa mostrata allo sblocco. */
|
|
celebrazione?: string;
|
|
/** Emoji usata nella celebrazione e nella notifica push. */
|
|
emoji?: string;
|
|
/** Testo della notifica push allo sblocco. */
|
|
notificaPush?: string;
|
|
};
|
|
|
|
export const badgeDefs: BadgeDef[] = [
|
|
{
|
|
id: "mvp",
|
|
nome: "MVP",
|
|
descrizione: "Riconoscimento per il miglior giocatore della partita, scelto dai compagni a fine match.",
|
|
unita: "MVP",
|
|
icon: Trophy,
|
|
soglie: { bronzo: 1, argento: 3, oro: 5 },
|
|
valore: (g) => g.mvp,
|
|
},
|
|
{
|
|
id: "pagella",
|
|
nome: "Pagellone",
|
|
descrizione: "Media dei voti che i compagni ti danno a fine partita: conta come giochi, non quanti punti fai.",
|
|
unita: "di media voto",
|
|
icon: ClipboardCheck,
|
|
soglie: { bronzo: 6.5, argento: 7.5, oro: 8.5 },
|
|
valore: (g) => g.mediaVoto,
|
|
},
|
|
{
|
|
id: "palloni",
|
|
nome: "Sherpa dei palloni",
|
|
descrizione: "Quante volte ti sei caricato la sacca dei palloni: lavoro oscuro, badge luminoso.",
|
|
unita: "turni palloni",
|
|
icon: CircleDot,
|
|
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
|
valore: (g) => g.palloni,
|
|
},
|
|
{
|
|
id: "presenze",
|
|
nome: "Presenza fissa",
|
|
descrizione: "Per chi è sempre lì, partita dopo partita, senza tirarsi indietro.",
|
|
unita: "presenze",
|
|
icon: Repeat,
|
|
soglie: { bronzo: 5, argento: 15, oro: 30 },
|
|
valore: (g) => g.presenze,
|
|
},
|
|
{
|
|
id: "serie-allenamenti",
|
|
nome: "Sempre in palestra",
|
|
descrizione: "Allenamenti consecutivi a cui sei stato presente: la costanza paga più del talento.",
|
|
unita: "allenamenti di fila",
|
|
icon: Rocket,
|
|
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
|
valore: (g) => g.serieAllenamenti,
|
|
},
|
|
{
|
|
id: "serie-conferme",
|
|
nome: "Risposta lampo",
|
|
descrizione: "Conferme rapide agli eventi: la squadra sa subito che ci sei.",
|
|
unita: "conferme entro 24h",
|
|
icon: Zap,
|
|
soglie: { bronzo: 3, argento: 8, oro: 15 },
|
|
valore: (g) => g.serieConferme,
|
|
},
|
|
];
|
|
|
|
/** Badge segreti: non compaiono nella UI finché non vengono sbloccati. */
|
|
export const badgeSegreti: BadgeDef[] = [
|
|
{
|
|
id: "s-tiebreak",
|
|
nome: "Uomo tie-break",
|
|
descrizione: "Sbloccato da chi ha almeno 2 MVP e una media voto alta: nei momenti caldi ci sei sempre.",
|
|
unita: "MVP con media alta",
|
|
icon: Ghost,
|
|
segreto: true,
|
|
soglie: { bronzo: 1, argento: 1, oro: 1 },
|
|
valore: (g) => (g.mvp >= 2 && g.mediaVoto >= 8 ? 1 : 0),
|
|
celebrazione: "Nei momenti caldi ci sei sempre.",
|
|
},
|
|
{
|
|
id: "s-mai-forfait",
|
|
nome: "Mai un forfait",
|
|
descrizione: "Sbloccato con 10 conferme rapide consecutive e 15 presenze: su di te la squadra può contare a occhi chiusi.",
|
|
unita: "requisito nascosto",
|
|
icon: Anchor,
|
|
segreto: true,
|
|
soglie: { bronzo: 1, argento: 1, oro: 1 },
|
|
valore: (g) => (g.serieConferme >= 10 && g.presenze >= 15 ? 1 : 0),
|
|
celebrazione: "Su di te la squadra può contare a occhi chiusi.",
|
|
},
|
|
{
|
|
id: "s-infermeria",
|
|
nome: "Cliente VIP dell'Infermeria",
|
|
descrizione:
|
|
"Hai saltato almeno 3 allenamenti o partite per infortunio. L'infermeria ormai ti aspetta con il caffè pronto ☕😂",
|
|
unita: "eventi da infortunato",
|
|
icon: Stethoscope,
|
|
segreto: true,
|
|
emoji: "🩺",
|
|
soglie: { bronzo: 1, argento: 1, oro: 1 },
|
|
valore: (g) => (g.infortuni >= 3 ? 1 : 0),
|
|
celebrazione: "L'infermeria ormai ti aspetta con il caffè pronto ☕😂",
|
|
notificaPush: "Speriamo che sia l'ultimo! 😅",
|
|
},
|
|
{
|
|
id: "s-ritardi",
|
|
nome: "Aspettate, arrivo!",
|
|
descrizione:
|
|
"Hai collezionato almeno 5 ritardi. Il tuo messaggio preferito è: 'Aspettate, arrivo!' 😅",
|
|
unita: "eventi in ritardo",
|
|
icon: AlarmClock,
|
|
segreto: true,
|
|
emoji: "⏰",
|
|
soglie: { bronzo: 1, argento: 1, oro: 1 },
|
|
valore: (g) => (g.ritardi >= 5 ? 1 : 0),
|
|
celebrazione:
|
|
"Hai collezionato almeno 5 ritardi. Il tuo messaggio preferito è: 'Aspettate, arrivo!' 😅",
|
|
notificaPush: "Forse è il momento di puntare la sveglia 10 minuti prima! 😄",
|
|
},
|
|
{
|
|
id: "s-cacche",
|
|
nome: "Trono di ferro",
|
|
descrizione:
|
|
"Almeno 3 partite di campionato affrontate con 3 o più cacche pre-gara. Il bagno del PalaCRAP porta il tuo nome 🚽😂",
|
|
unita: "partite da record",
|
|
icon: Toilet,
|
|
segreto: true,
|
|
emoji: "🚽",
|
|
soglie: { bronzo: 1, argento: 1, oro: 1 },
|
|
valore: (g) => (g.cacche >= 3 ? 1 : 0),
|
|
celebrazione: "Il bagno del PalaCRAP porta ufficialmente il tuo nome 🚽😂",
|
|
notificaPush: "Scarico completo: badge segreto sbloccato! 😄",
|
|
},
|
|
];
|
|
|
|
export const tuttiBadge: BadgeDef[] = [...badgeDefs, ...badgeSegreti];
|
|
|
|
export const iconaSegreto = Lock;
|
|
|
|
export type BadgeStato = {
|
|
def: BadgeDef;
|
|
valore: number;
|
|
grado: Grado | null;
|
|
prossimo: Grado | null;
|
|
prossimaSoglia: number | null;
|
|
progresso: number;
|
|
};
|
|
|
|
export function gradoRaggiunto(def: BadgeDef, valore: number): Grado | null {
|
|
let grado: Grado | null = null;
|
|
for (const g of gradiOrdine) if (valore >= def.soglie[g]) grado = g;
|
|
return grado;
|
|
}
|
|
|
|
export function statoBadge(def: BadgeDef, g: Giocatore): BadgeStato {
|
|
const valore = def.valore(g);
|
|
const grado = gradoRaggiunto(def, valore);
|
|
const prossimo = gradiOrdine.find((x) => valore < def.soglie[x]) ?? null;
|
|
const prossimaSoglia = prossimo ? def.soglie[prossimo] : null;
|
|
const progresso = prossimaSoglia
|
|
? Math.min(100, Math.round((valore / prossimaSoglia) * 100))
|
|
: 100;
|
|
return { def, valore, grado, prossimo, prossimaSoglia, progresso };
|
|
}
|
|
|
|
export function badgeGiocatore(g: Giocatore): BadgeStato[] {
|
|
return badgeDefs.map((def) => statoBadge(def, g));
|
|
}
|
|
|
|
/** Badge segreti già sbloccati dal giocatore. */
|
|
export function badgeSegretiSbloccati(g: Giocatore): BadgeStato[] {
|
|
return badgeSegreti.map((def) => statoBadge(def, g)).filter((b) => b.grado !== null);
|
|
}
|
|
|
|
/** Quanti segreti restano da scoprire. */
|
|
export function segretiNascosti(g: Giocatore) {
|
|
return badgeSegreti.length - badgeSegretiSbloccati(g).length;
|
|
}
|
|
|
|
export type Collezione = {
|
|
sbloccati: BadgeStato[];
|
|
inProgresso: BadgeStato[];
|
|
segreti: BadgeStato[];
|
|
nascosti: number;
|
|
totali: number;
|
|
ottenuti: number;
|
|
};
|
|
|
|
export function collezioneBadge(g: Giocatore): Collezione {
|
|
const normali = badgeGiocatore(g);
|
|
const segreti = badgeSegretiSbloccati(g);
|
|
const sbloccati = normali.filter((b) => b.grado !== null);
|
|
return {
|
|
sbloccati,
|
|
inProgresso: normali.filter((b) => b.grado === null),
|
|
segreti,
|
|
nascosti: badgeSegreti.length - segreti.length,
|
|
totali: tuttiBadge.length,
|
|
ottenuti: sbloccati.length + segreti.length,
|
|
};
|
|
}
|
|
|
|
/** Il traguardo più vicino: il badge a cui manca meno, in proporzione. */
|
|
export function prossimoTraguardo(g: Giocatore): BadgeStato | null {
|
|
const candidati = badgeGiocatore(g).filter((b) => b.prossimaSoglia !== null);
|
|
if (candidati.length === 0) return null;
|
|
return candidati.sort((a, b) => b.progresso - a.progresso)[0]!;
|
|
}
|
|
|
|
/** Riga "Ti mancano X per il prossimo livello". */
|
|
export function mancanoPer(b: BadgeStato) {
|
|
if (!b.prossimaSoglia || !b.prossimo) return "Livello massimo raggiunto";
|
|
const manca = b.prossimaSoglia - b.valore;
|
|
return `Ti mancano ${manca} ${b.def.unita} per il ${gradoMeta[b.prossimo].label.toLowerCase()}`;
|
|
}
|
|
|
|
/** Microcopy motivazionale in base a quanto sei vicino. */
|
|
export function microcopyBadge(b: BadgeStato) {
|
|
if (!b.prossimaSoglia) return "Hai fatto tutto: badge d'oro in bacheca.";
|
|
if (b.progresso >= 90) return "Ci sei quasi: un ultimo sforzo!";
|
|
if (b.progresso >= 60) return "Sei in piena corsa, continua così.";
|
|
if (b.progresso >= 25) return "Buon ritmo, il prossimo livello si avvicina.";
|
|
return "Ogni partita conta: si parte da qui.";
|
|
}
|
|
|
|
export function badgeSbloccati(g: Giocatore): BadgeStato[] {
|
|
return badgeGiocatore(g).filter((b) => b.grado !== null);
|
|
}
|
|
|
|
export function descrizioneSoglie(def: BadgeDef) {
|
|
return `${def.soglie.bronzo}/${def.soglie.argento}/${def.soglie.oro} ${def.unita}`;
|
|
} |