2026-06-09 23:41:33 +02:00
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
requiredItemQuantityMax: 3,
|
2026-06-10 00:11:33 +02:00
|
|
|
/**
|
|
|
|
|
* Carattere meccanico di ogni tipo di missione:
|
|
|
|
|
* - moneyFactor/xpFactor/repFactor: moltiplicatori delle ricompense
|
|
|
|
|
* - riskShift: spostamento del rischio base
|
|
|
|
|
* - requiredItemChance: probabilità che serva merce dall'inventario
|
|
|
|
|
* - lootChance/lootQuantityMax: probabilità e quantità del bottino in merce
|
|
|
|
|
*/
|
|
|
|
|
typeConfig: {
|
|
|
|
|
DELIVERY: {
|
|
|
|
|
moneyFactor: 1.0,
|
|
|
|
|
xpFactor: 1.0,
|
|
|
|
|
repFactor: 1,
|
|
|
|
|
riskShift: -0.05,
|
|
|
|
|
requiredItemChance: 0.6,
|
|
|
|
|
lootChance: 0,
|
|
|
|
|
lootQuantityMax: 0,
|
|
|
|
|
},
|
|
|
|
|
THEFT: {
|
|
|
|
|
moneyFactor: 0.6,
|
|
|
|
|
xpFactor: 1.0,
|
|
|
|
|
repFactor: 1,
|
|
|
|
|
riskShift: 0.05,
|
|
|
|
|
requiredItemChance: 0,
|
|
|
|
|
lootChance: 0.8,
|
|
|
|
|
lootQuantityMax: 2,
|
|
|
|
|
},
|
|
|
|
|
SMUGGLING: {
|
|
|
|
|
moneyFactor: 1.6,
|
|
|
|
|
xpFactor: 0.9,
|
|
|
|
|
repFactor: 1,
|
|
|
|
|
riskShift: 0.1,
|
|
|
|
|
requiredItemChance: 0.5,
|
|
|
|
|
lootChance: 0.25,
|
|
|
|
|
lootQuantityMax: 1,
|
|
|
|
|
},
|
|
|
|
|
INTEL: {
|
|
|
|
|
moneyFactor: 0.5,
|
|
|
|
|
xpFactor: 1.8,
|
|
|
|
|
repFactor: 2,
|
|
|
|
|
riskShift: -0.1,
|
|
|
|
|
requiredItemChance: 0,
|
|
|
|
|
lootChance: 0,
|
|
|
|
|
lootQuantityMax: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
/** livello minimo richiesto, indicizzato per difficoltà (1-5) */
|
|
|
|
|
minLevelByDifficulty: [1, 1, 1, 2, 4, 6],
|
|
|
|
|
/**
|
|
|
|
|
* Multa sul fallimento: rewardMoney * rewardFactor * pressione polizia,
|
|
|
|
|
* mai oltre il denaro posseduto dal giocatore.
|
|
|
|
|
*/
|
|
|
|
|
failFine: {
|
|
|
|
|
rewardFactor: 0.3,
|
|
|
|
|
},
|
|
|
|
|
/** abbandono volontario di una missione attiva */
|
|
|
|
|
abandon: {
|
|
|
|
|
reputationLoss: 1,
|
|
|
|
|
},
|
2026-06-09 23:41:33 +02:00
|
|
|
/**
|
|
|
|
|
* 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;
|