Badge e obiettivi: soglie in costanti, obiettivi a vittorie da una sola funzione

Soglie dei badge segreti e target presenze (90) in costanti usate anche nei
testi; o3/o4/o5 generati da obiettivoVittorie. Unica differenza visibile: la
celebrazione di «Aspettate, arrivo!» non ripete più la descrizione.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-11-15 19:43:24 +01:00
1 parent 99265f6a1e
commit 50930ac142
2 files changed
+75 -85

No files matched your search

+28 -18
View File
@@ -62,6 +62,16 @@ export type BadgeDef = {
*/
export const VOTI_MINIMI_PAGELLA = 5;
/** Soglie dei badge segreti: usate sia per sbloccarli sia nei loro testi. */
const SEGRETO_TIEBREAK_MVP = 2;
const SEGRETO_TIEBREAK_MEDIA = 8;
const SEGRETO_INFORTUNI = 3;
const SEGRETO_RITARDI = 5;
const SEGRETO_CACCHE_PARTITE = 3;
/** I badge segreti sono tutto-o-niente: una sola soglia, uguale per i tre gradi. */
const unaSoglia: Record<Grado, number> = { bronzo: 1, argento: 1, oro: 1 };
export const badgeDefs: BadgeDef[] = [
{
id: "mvp",
@@ -119,57 +129,57 @@ 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.",
descrizione: `Sbloccato da chi ha almeno ${SEGRETO_TIEBREAK_MVP} 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 },
soglie: unaSoglia,
// Stessa soglia minima di voti del badge Pagellone: sotto VOTI_MINIMI_PAGELLA la media
// non è statisticamente significativa, non deve poter sbloccare nemmeno questo segreto.
valore: (g) => (g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8 ? 1 : 0),
valore: (g) =>
g.mvp >= SEGRETO_TIEBREAK_MVP &&
g.votiPagella >= VOTI_MINIMI_PAGELLA &&
g.mediaVoto >= SEGRETO_TIEBREAK_MEDIA
? 1
: 0,
celebrazione: "Nei momenti caldi ci sei sempre.",
},
{
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 ☕😂",
descrizione: `Hai saltato almeno ${SEGRETO_INFORTUNI} 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),
soglie: unaSoglia,
valore: (g) => (g.infortuni >= SEGRETO_INFORTUNI ? 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!' 😅",
descrizione: `Hai collezionato almeno ${SEGRETO_RITARDI} 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!' 😅",
soglie: unaSoglia,
valore: (g) => (g.ritardi >= SEGRETO_RITARDI ? 1 : 0),
celebrazione: "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 affrontate con 3 o più cacche pre-gara (campionato o amichevole, qui non si fanno sconti). Il bagno del PalaCRAP porta il tuo nome 🚽😂",
descrizione: `Almeno ${SEGRETO_CACCHE_PARTITE} partite affrontate con 3 o più cacche pre-gara (campionato o amichevole, qui non si fanno sconti). 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),
soglie: unaSoglia,
valore: (g) => (g.cacche >= SEGRETO_CACCHE_PARTITE ? 1 : 0),
celebrazione: "Il bagno del PalaCRAP porta ufficialmente il tuo nome 🚽😂",
notificaPush: "Scarico completo: badge segreto sbloccato! 😄",
},
+47 -67
View File
@@ -124,6 +124,37 @@ function percentualeRisposte(ctx: ContestoObiettivi, rosaSize: number) {
return Math.round((risposte / posti) * 100);
}
/** Percentuale di presenze (e di risposte) da raggiungere negli obiettivi o1, o2 e o7. */
const TARGET_PRESENZE = 90;
/** Obiettivi a vittorie: stessa spiegazione, cambiano solo target e testi. */
function obiettivoVittorie(
vittorie: number,
o: Pick<ObiettivoSquadra, "id" | "titolo" | "descrizione" | "emoji" | "impatto" | "target"> & {
completaCon: string;
},
): ObiettivoSquadra {
const { completaCon, ...base } = o;
return {
...base,
valore: Math.min(vittorie, o.target),
unita: "vittorie",
dettaglio: {
comeSiCalcola: `Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con ${completaCon}.`,
conta: ["Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari."],
nonConta: [
"Le sconfitte e le partite non ancora giocate.",
"Amichevoli e allenamenti.",
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
],
periodo: "Tutta la stagione di campionato. Non si azzera.",
fonte:
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
},
};
}
/** Obiettivi collaborativi: si muovono con il contributo di tutta la rosa. */
export function obiettiviSquadra(
rosa: Giocatore[] = giocatori,
@@ -136,10 +167,10 @@ export function obiettiviSquadra(
return [
{
id: "o1",
titolo: `90% di presenze ${aMese(oggi)}`,
titolo: `${TARGET_PRESENZE}% di presenze ${aMese(oggi)}`,
descrizione: "Media presenze su partite e allenamenti del mese",
valore: percentualePresenze(ctx, rosa, (e) => e.data.startsWith(mese)),
target: 90,
target: TARGET_PRESENZE,
unita: "%",
scadenza: fineMese(oggi),
emoji: "📣",
@@ -173,7 +204,7 @@ export function obiettiviSquadra(
titolo: "Tutti rispondono alle convocazioni",
descrizione: "Percentuale di risposte date sugli eventi in programma",
valore: percentualeRisposte(ctx, rosa.length),
target: 90,
target: TARGET_PRESENZE,
unita: "%",
scadenza: fineMese(oggi),
emoji: "⚡",
@@ -201,7 +232,7 @@ export function obiettiviSquadra(
titolo: "Presenze collettive",
descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione",
valore: percentualePresenze(ctx, rosa),
target: 90,
target: TARGET_PRESENZE,
unita: "%",
emoji: "🤝",
impatto: "Ogni allenamento a cui vieni vale +1 per il gruppo.",
@@ -296,84 +327,33 @@ export function obiettiviSquadra(
"Il target è 12 perché è il numero minimo per un allenamento 6 contro 6. Con 9 giocatori in serie ne mancano 3.",
},
},
{
obiettivoVittorie(vittorie, {
id: "o3",
target: 1,
titolo: "Prima vittoria del campionato",
descrizione: "Sbloccare la stagione con i primi 3 punti",
valore: Math.min(vittorie, 1),
target: 1,
unita: "vittorie",
emoji: "🎉",
impatto: "Una prestazione di gruppo e il primo passo è fatto.",
dettaglio: {
comeSiCalcola:
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con la prima vittoria.",
conta: [
"Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari.",
],
nonConta: [
"Le sconfitte e le partite non ancora giocate.",
"Amichevoli e allenamenti.",
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
],
periodo: "Tutta la stagione di campionato. Non si azzera.",
fonte:
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
},
},
{
completaCon: "la prima vittoria",
}),
obiettivoVittorie(vittorie, {
id: "o4",
target: 5,
titolo: "5 vittorie in campionato",
descrizione: "Metà strada verso la zona playoff",
valore: Math.min(vittorie, 5),
target: 5,
unita: "vittorie",
emoji: "🔥",
impatto: "Ogni vittoria ci avvicina ai playoff.",
dettaglio: {
comeSiCalcola:
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con 5 vittorie.",
conta: [
"Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari.",
],
nonConta: [
"Le sconfitte e le partite non ancora giocate.",
"Amichevoli e allenamenti.",
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
],
periodo: "Tutta la stagione di campionato. Non si azzera.",
fonte:
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
},
},
{
completaCon: "5 vittorie",
}),
obiettivoVittorie(vittorie, {
id: "o5",
target: 10,
titolo: "10 vittorie in campionato",
descrizione: "Obiettivo stagionale per il podio",
valore: Math.min(vittorie, 10),
target: 10,
unita: "vittorie",
emoji: "🏆",
impatto: "L'obiettivo grande: serve tutta la stagione insieme.",
dettaglio: {
comeSiCalcola:
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con 10 vittorie.",
conta: [
"Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari.",
],
nonConta: [
"Le sconfitte e le partite non ancora giocate.",
"Amichevoli e allenamenti.",
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
],
periodo: "Tutta la stagione di campionato. Non si azzera.",
fonte:
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
},
},
completaCon: "10 vittorie",
}),
{
id: "o6",
titolo: "1 evento di squadra al mese",