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; 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[] = [ export const badgeDefs: BadgeDef[] = [
{ {
id: "mvp", id: "mvp",
@@ -119,57 +129,57 @@ export const badgeSegreti: BadgeDef[] = [
{ {
id: "s-tiebreak", id: "s-tiebreak",
nome: "Uomo tie-break", nome: "Uomo tie-break",
descrizione: descrizione: `Sbloccato da chi ha almeno ${SEGRETO_TIEBREAK_MVP} MVP e una media voto alta: nei momenti caldi ci sei sempre.`,
"Sbloccato da chi ha almeno 2 MVP e una media voto alta: nei momenti caldi ci sei sempre.",
unita: "MVP con media alta", unita: "MVP con media alta",
icon: Ghost, icon: Ghost,
segreto: true, 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 // Stessa soglia minima di voti del badge Pagellone: sotto VOTI_MINIMI_PAGELLA la media
// non è statisticamente significativa, non deve poter sbloccare nemmeno questo segreto. // 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.", celebrazione: "Nei momenti caldi ci sei sempre.",
}, },
{ {
id: "s-infermeria", id: "s-infermeria",
nome: "Cliente VIP dell'Infermeria", nome: "Cliente VIP dell'Infermeria",
descrizione: descrizione: `Hai saltato almeno ${SEGRETO_INFORTUNI} allenamenti o partite per infortunio. L'infermeria ormai ti aspetta con il caffè pronto ☕😂`,
"Hai saltato almeno 3 allenamenti o partite per infortunio. L'infermeria ormai ti aspetta con il caffè pronto ☕😂",
unita: "eventi da infortunato", unita: "eventi da infortunato",
icon: Stethoscope, icon: Stethoscope,
segreto: true, segreto: true,
emoji: "🩺", emoji: "🩺",
soglie: { bronzo: 1, argento: 1, oro: 1 }, soglie: unaSoglia,
valore: (g) => (g.infortuni >= 3 ? 1 : 0), valore: (g) => (g.infortuni >= SEGRETO_INFORTUNI ? 1 : 0),
celebrazione: "L'infermeria ormai ti aspetta con il caffè pronto ☕😂", celebrazione: "L'infermeria ormai ti aspetta con il caffè pronto ☕😂",
notificaPush: "Speriamo che sia l'ultimo! 😅", notificaPush: "Speriamo che sia l'ultimo! 😅",
}, },
{ {
id: "s-ritardi", id: "s-ritardi",
nome: "Aspettate, arrivo!", nome: "Aspettate, arrivo!",
descrizione: descrizione: `Hai collezionato almeno ${SEGRETO_RITARDI} ritardi. Il tuo messaggio preferito è: 'Aspettate, arrivo!' 😅`,
"Hai collezionato almeno 5 ritardi. Il tuo messaggio preferito è: 'Aspettate, arrivo!' 😅",
unita: "eventi in ritardo", unita: "eventi in ritardo",
icon: AlarmClock, icon: AlarmClock,
segreto: true, segreto: true,
emoji: "⏰", emoji: "⏰",
soglie: { bronzo: 1, argento: 1, oro: 1 }, soglie: unaSoglia,
valore: (g) => (g.ritardi >= 5 ? 1 : 0), valore: (g) => (g.ritardi >= SEGRETO_RITARDI ? 1 : 0),
celebrazione: celebrazione: "Il tuo messaggio preferito è: 'Aspettate, arrivo!' 😅",
"Hai collezionato almeno 5 ritardi. Il tuo messaggio preferito è: 'Aspettate, arrivo!' 😅",
notificaPush: "Forse è il momento di puntare la sveglia 10 minuti prima! 😄", notificaPush: "Forse è il momento di puntare la sveglia 10 minuti prima! 😄",
}, },
{ {
id: "s-cacche", id: "s-cacche",
nome: "Trono di ferro", nome: "Trono di ferro",
descrizione: 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 🚽😂`,
"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 🚽😂",
unita: "partite da record", unita: "partite da record",
icon: Toilet, icon: Toilet,
segreto: true, segreto: true,
emoji: "🚽", emoji: "🚽",
soglie: { bronzo: 1, argento: 1, oro: 1 }, soglie: unaSoglia,
valore: (g) => (g.cacche >= 3 ? 1 : 0), valore: (g) => (g.cacche >= SEGRETO_CACCHE_PARTITE ? 1 : 0),
celebrazione: "Il bagno del PalaCRAP porta ufficialmente il tuo nome 🚽😂", celebrazione: "Il bagno del PalaCRAP porta ufficialmente il tuo nome 🚽😂",
notificaPush: "Scarico completo: badge segreto sbloccato! 😄", 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); 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. */ /** Obiettivi collaborativi: si muovono con il contributo di tutta la rosa. */
export function obiettiviSquadra( export function obiettiviSquadra(
rosa: Giocatore[] = giocatori, rosa: Giocatore[] = giocatori,
@@ -136,10 +167,10 @@ export function obiettiviSquadra(
return [ return [
{ {
id: "o1", id: "o1",
titolo: `90% di presenze ${aMese(oggi)}`, titolo: `${TARGET_PRESENZE}% di presenze ${aMese(oggi)}`,
descrizione: "Media presenze su partite e allenamenti del mese", descrizione: "Media presenze su partite e allenamenti del mese",
valore: percentualePresenze(ctx, rosa, (e) => e.data.startsWith(mese)), valore: percentualePresenze(ctx, rosa, (e) => e.data.startsWith(mese)),
target: 90, target: TARGET_PRESENZE,
unita: "%", unita: "%",
scadenza: fineMese(oggi), scadenza: fineMese(oggi),
emoji: "📣", emoji: "📣",
@@ -173,7 +204,7 @@ export function obiettiviSquadra(
titolo: "Tutti rispondono alle convocazioni", titolo: "Tutti rispondono alle convocazioni",
descrizione: "Percentuale di risposte date sugli eventi in programma", descrizione: "Percentuale di risposte date sugli eventi in programma",
valore: percentualeRisposte(ctx, rosa.length), valore: percentualeRisposte(ctx, rosa.length),
target: 90, target: TARGET_PRESENZE,
unita: "%", unita: "%",
scadenza: fineMese(oggi), scadenza: fineMese(oggi),
emoji: "⚡", emoji: "⚡",
@@ -201,7 +232,7 @@ export function obiettiviSquadra(
titolo: "Presenze collettive", titolo: "Presenze collettive",
descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione", descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione",
valore: percentualePresenze(ctx, rosa), valore: percentualePresenze(ctx, rosa),
target: 90, target: TARGET_PRESENZE,
unita: "%", unita: "%",
emoji: "🤝", emoji: "🤝",
impatto: "Ogni allenamento a cui vieni vale +1 per il gruppo.", 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.", "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", id: "o3",
target: 1,
titolo: "Prima vittoria del campionato", titolo: "Prima vittoria del campionato",
descrizione: "Sbloccare la stagione con i primi 3 punti", descrizione: "Sbloccare la stagione con i primi 3 punti",
valore: Math.min(vittorie, 1),
target: 1,
unita: "vittorie",
emoji: "🎉", emoji: "🎉",
impatto: "Una prestazione di gruppo e il primo passo è fatto.", impatto: "Una prestazione di gruppo e il primo passo è fatto.",
dettaglio: { completaCon: "la prima vittoria",
comeSiCalcola: }),
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con la prima vittoria.", obiettivoVittorie(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.",
},
},
{
id: "o4", id: "o4",
target: 5,
titolo: "5 vittorie in campionato", titolo: "5 vittorie in campionato",
descrizione: "Metà strada verso la zona playoff", descrizione: "Metà strada verso la zona playoff",
valore: Math.min(vittorie, 5),
target: 5,
unita: "vittorie",
emoji: "🔥", emoji: "🔥",
impatto: "Ogni vittoria ci avvicina ai playoff.", impatto: "Ogni vittoria ci avvicina ai playoff.",
dettaglio: { completaCon: "5 vittorie",
comeSiCalcola: }),
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con 5 vittorie.", obiettivoVittorie(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.",
},
},
{
id: "o5", id: "o5",
target: 10,
titolo: "10 vittorie in campionato", titolo: "10 vittorie in campionato",
descrizione: "Obiettivo stagionale per il podio", descrizione: "Obiettivo stagionale per il podio",
valore: Math.min(vittorie, 10),
target: 10,
unita: "vittorie",
emoji: "🏆", emoji: "🏆",
impatto: "L'obiettivo grande: serve tutta la stagione insieme.", impatto: "L'obiettivo grande: serve tutta la stagione insieme.",
dettaglio: { completaCon: "10 vittorie",
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.",
},
},
{ {
id: "o6", id: "o6",
titolo: "1 evento di squadra al mese", titolo: "1 evento di squadra al mese",