Files
gioco-mobile/server/src/config/gameBalance.ts
T
davide 8ad83348f4 server: core bootstrap — config, db clients, shared utilities
Add env config (zod validation), game balance constants, Prisma and
Redis client singletons, auth guard middleware, error helpers,
request validators, Fastify JWT type augmentation, and app/server
entry points.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 23:41:33 +02:00

157 lines
5.2 KiB
TypeScript

/**
* Bilanciamento di gioco — TUTTI i parametri numerici delle regole vivono qui.
*
* Nessuna formula nel resto del codice deve usare costanti "magiche":
* se serve un numero, va aggiunto in questo file e importato.
*/
export const balance = {
/** Parametri del giocatore */
player: {
/** Città di partenza dei nuovi giocatori (deve esistere nel seed) */
startingCityName: 'Milano',
},
/**
* Progressione esperienza.
* XP necessari per passare dal livello L al livello L+1:
* xpForNextLevel(L) = base * L ^ exponent
* Es. livello 1→2: 100 XP, 2→3: ~283 XP, 3→4: ~520 XP.
*/
xp: {
base: 100,
exponent: 1.5,
},
/**
* Viaggio tra città.
* Costo = baseCost + riskLevel(città di destinazione) * costPerRiskLevel
* Es. città con riskLevel 3: 50 + 3*25 = 125.
*/
travel: {
baseCost: 50,
costPerRiskLevel: 25,
},
/** Mercato dinamico (vedi blueprint §9) */
market: {
/** buyPrice = round(rawPrice * buyMargin) */
buyMargin: 1.12,
/** sellPrice = round(rawPrice * sellMargin) */
sellMargin: 0.88,
/** rawPrice è limitato a [basePrice*minPriceFactor, basePrice*maxPriceFactor] */
minPriceFactor: 0.35,
maxPriceFactor: 3.5,
/** divisore minimo dell'offerta nella formula: demand / max(supply, minSupplyDivisor) */
minSupplyDivisor: 0.25,
/** demand e supply restano sempre in [min, max] */
minDemandSupply: 0.25,
maxDemandSupply: 4.0,
/** variazioni per unità comprata */
demandDeltaPerBuyUnit: 0.02,
supplyDeltaPerBuyUnit: -0.01,
/** variazioni per unità venduta */
demandDeltaPerSellUnit: -0.01,
supplyDeltaPerSellUnit: 0.02,
/** ritorno verso l'equilibrio a ogni tick: v = v*decayFactor + equilibrium*(1-decayFactor) */
decayFactor: 0.98,
equilibrium: 1.0,
/** massimo numero di unità per singola transazione */
maxQuantityPerTrade: 100,
},
/** Generazione e risoluzione missioni */
missions: {
/** numero minimo di missioni attive (non scadute) per città */
minActivePerCity: 5,
/** missioni attive contemporanee per giocatore */
maxActivePerPlayer: 3,
/** scadenza missione generata: minuti casuali in [min, max] */
expiryMinutesMin: 30,
expiryMinutesMax: 60,
/** durata missione: secondi casuali in [min, max] */
durationSecondsMin: 60,
durationSecondsMax: 900,
/** difficoltà casuale in [min, max] */
difficultyMin: 1,
difficultyMax: 5,
/**
* Rischio della missione (0..1):
* risk = difficulty*riskPerDifficulty + (cityRiskLevel-3)*riskPerCityRiskLevel ± riskJitter
* limitato a [riskMin, riskMax].
*/
riskPerDifficulty: 0.12,
riskPerCityRiskLevel: 0.03,
riskJitter: 0.05,
riskMin: 0.05,
riskMax: 0.9,
/**
* Ricompensa in denaro:
* reward = (base + difficulty*perDifficulty) * economyModifier * (1 + risk*riskRewardBonus)
* Se la missione richiede oggetti, si aggiunge il loro valore * requiredItemRefund.
*/
rewardMoneyBase: 150,
rewardMoneyPerDifficulty: 250,
riskRewardBonus: 0.5,
requiredItemRefund: 1.15,
/** Ricompensa XP: base + difficulty * perDifficulty */
rewardXpBase: 20,
rewardXpPerDifficulty: 30,
/** probabilità che una missione DELIVERY/SMUGGLING richieda oggetti */
requiredItemChance: 0.4,
requiredItemQuantityMax: 3,
/**
* Probabilità di successo al claim:
* chance = baseChance
* - risk * riskWeight
* + level * levelBonus
* + reputation * reputationBonus
* - (policePressure*eventPoliceModifier - 1) * policeWeight
* limitata a [minChance, maxChance].
*/
success: {
baseChance: 0.95,
riskWeight: 0.6,
levelBonus: 0.01,
reputationBonus: 0.0005,
policeWeight: 0.05,
minChance: 0.05,
maxChance: 0.95,
},
/** reputazione: +difficulty*gain in caso di successo, -loss in caso di fallimento (mai sotto 0) */
reputationGainPerDifficulty: 2,
reputationLossOnFail: 3,
},
/** Eventi mondo (locali e globali) */
events: {
/** probabilità di generare un evento locale a ogni ciclo da 15 minuti */
localChance: 0.25,
/** probabilità di generare un evento globale a ogni ciclo orario */
globalChance: 0.2,
/** durata eventi locali: minuti casuali in [min, max] */
localDurationMinutesMin: 15,
localDurationMinutesMax: 45,
/** durata eventi globali: minuti casuali in [min, max] */
globalDurationMinutesMin: 30,
globalDurationMinutesMax: 90,
/** range [min, max] dei moltiplicatori per tipo di evento */
types: {
MARKET_BOOM: { priceModifier: [1.2, 1.5], policeModifier: [1.0, 1.0] },
POLICE_RAID: { priceModifier: [0.9, 1.1], policeModifier: [1.3, 1.8] },
SHORTAGE: { priceModifier: [1.3, 1.8], policeModifier: [1.0, 1.2] },
BLACKOUT: { priceModifier: [0.7, 0.9], policeModifier: [0.8, 1.0] },
},
},
/** Classifiche */
leaderboard: {
/** numero massimo di posizioni restituite/memorizzate */
maxEntries: 100,
/** limite di default per le richieste GET */
defaultLimit: 20,
},
} as const;
export type EventTypeBalance = keyof typeof balance.events.types;