server: scheduled background jobs
Add node-cron jobs for periodic market price updates, mission generation, and world event generation; all wired through a central scheduler module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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<EventTypeBalance, { title: string; description: string }> = {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user