diff --git a/src/lib/badges.ts b/src/lib/badges.ts index 6999cff..60b2ae2 100644 --- a/src/lib/badges.ts +++ b/src/lib/badges.ts @@ -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 = { 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! 😄", }, diff --git a/src/lib/obiettivi.ts b/src/lib/obiettivi.ts index ac0e681..6aec795 100644 --- a/src/lib/obiettivi.ts +++ b/src/lib/obiettivi.ts @@ -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 & { + 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",