import { EventType } from '@prisma/client'; import { balance, type EventTypeBalance } from '../config/gameBalance.js'; import { prisma } from '../db/prisma.js'; import { redis } from '../db/redis.js'; import { toEventDto } from '../modules/events/events.service.js'; import { emitGlobal } from '../realtime/socket.js'; import { pickRandom, randomFloat, randomInt } from '../shared/validators.js'; /** Testi degli eventi per tipo (i moltiplicatori numerici sono in gameBalance). */ const EVENT_TEXTS: Record = { MARKET_BOOM: { title: 'Boom di mercato', description: 'La domanda esplode: i prezzi salgono in tutta la zona.', }, POLICE_RAID: { title: 'Retata della polizia', description: 'Controlli ovunque: muoversi è molto più rischioso.', }, SHORTAGE: { title: 'Carenza di merci', description: 'Le scorte scarseggiano e i prezzi schizzano alle stelle.', }, BLACKOUT: { title: 'Blackout', description: 'La città è al buio: il mercato rallenta, la polizia pure.', }, }; async function createEvent(cityId: string | null, durationMinutes: number): Promise { const type = pickRandom(Object.values(EventType)); const ranges = balance.events.types[type]; const texts = EVENT_TEXTS[type]; const now = new Date(); const event = await prisma.worldEvent.create({ data: { title: texts.title, description: texts.description, type, cityId, priceModifier: Math.round(randomFloat(ranges.priceModifier[0], ranges.priceModifier[1]) * 100) / 100, policeModifier: Math.round(randomFloat(ranges.policeModifier[0], ranges.policeModifier[1]) * 100) / 100, startsAt: now, endsAt: new Date(now.getTime() + durationMinutes * 60_000), }, }); emitGlobal('worldEvent:started', toEventDto(event)); } /** Ogni 15 minuti: possibile evento locale in una città casuale. */ export async function maybeGenerateLocalEvent(): Promise { if (Math.random() >= balance.events.localChance) return; const cities = await prisma.city.findMany({ select: { id: true } }); if (cities.length === 0) return; await createEvent( pickRandom(cities).id, randomInt(balance.events.localDurationMinutesMin, balance.events.localDurationMinutesMax), ); } /** Ogni ora: possibile evento globale (cityId null). */ export async function maybeGenerateGlobalEvent(): Promise { if (Math.random() >= balance.events.globalChance) return; await createEvent( null, randomInt(balance.events.globalDurationMinutesMin, balance.events.globalDurationMinutesMax), ); } /** Notifica (una sola volta) la fine degli eventi terminati di recente. */ export async function notifyEndedEvents(): Promise { const now = new Date(); const recentlyEnded = await prisma.worldEvent.findMany({ where: { endsAt: { lt: now, gt: new Date(now.getTime() - 2 * 60 * 60_000) }, }, }); for (const event of recentlyEnded) { const firstTime = await redis.set(`notify:event-end:${event.id}`, '1', 'EX', 86_400, 'NX'); if (firstTime) { emitGlobal('worldEvent:ended', toEventDto(event)); } } }