Files
gioco-mobile/server/src/modules/market/market.service.ts
T

181 lines
5.5 KiB
TypeScript
Raw Normal View History

import { balance } from '../../config/gameBalance.js';
import { prisma } from '../../db/prisma.js';
import { errors } from '../../shared/errors.js';
import {
getActiveEvents,
eventsForCity,
priceModifierForCity,
toEventDto,
} from '../events/events.service.js';
import { getPlayerOrThrow } from '../players/players.service.js';
import { clampDemandSupply, computePrices } from './pricing.js';
export type TradeResult = {
itemId: string;
itemName: string;
quantity: number;
unitPrice: number;
total: number;
playerMoney: number;
};
/** Listino del mercato della città in cui si trova il giocatore. */
export async function getCurrentMarket(playerId: string) {
const player = await prisma.player.findUnique({
where: { id: playerId },
include: { currentCity: true },
});
if (!player) throw errors.notFound('Giocatore non trovato');
const [prices, events] = await Promise.all([
prisma.marketPrice.findMany({
where: { cityId: player.currentCityId },
include: { item: true },
orderBy: { item: { name: 'asc' } },
}),
getActiveEvents(),
]);
return {
city: {
id: player.currentCity.id,
name: player.currentCity.name,
riskLevel: player.currentCity.riskLevel,
economyModifier: player.currentCity.economyModifier,
},
prices: prices.map((p) => ({
itemId: p.itemId,
name: p.item.name,
buyPrice: p.buyPrice,
sellPrice: p.sellPrice,
demand: p.demand,
supply: p.supply,
rarity: p.item.rarity,
illegalLevel: p.item.illegalLevel,
weight: p.item.weight,
updatedAt: p.updatedAt,
})),
activeEvents: eventsForCity(events, player.currentCityId).map(toEventDto),
};
}
/**
* Acquisto: il prezzo lo decide il server (buyPrice corrente).
* Aggiorna denaro, inventario, domanda/offerta e ricalcola subito i prezzi.
*/
export async function buyItem(
playerId: string,
itemId: string,
quantity: number,
): Promise<TradeResult> {
const m = balance.market;
return prisma.$transaction(async (tx) => {
const player = await getPlayerOrThrow(playerId, tx);
const price = await tx.marketPrice.findUnique({
where: { cityId_itemId: { cityId: player.currentCityId, itemId } },
include: { item: true, city: true },
});
if (!price) throw errors.notFound('Oggetto non disponibile in questo mercato');
const total = price.buyPrice * quantity;
if (player.money < total) throw errors.insufficientFunds();
await tx.player.update({
where: { id: player.id },
data: { money: { decrement: total } },
});
await tx.inventoryItem.upsert({
where: { playerId_itemId: { playerId, itemId } },
create: { playerId, itemId, quantity },
update: { quantity: { increment: quantity } },
});
const demand = clampDemandSupply(price.demand + quantity * m.demandDeltaPerBuyUnit);
const supply = clampDemandSupply(price.supply + quantity * m.supplyDeltaPerBuyUnit);
const events = await getActiveEvents(tx);
const next = computePrices(
price.item.basePrice,
price.city.economyModifier,
demand,
supply,
priceModifierForCity(events, price.cityId),
);
await tx.marketPrice.update({
where: { cityId_itemId: { cityId: price.cityId, itemId } },
data: { demand, supply, buyPrice: next.buyPrice, sellPrice: next.sellPrice },
});
return {
itemId,
itemName: price.item.name,
quantity,
unitPrice: price.buyPrice,
total,
playerMoney: player.money - total,
};
});
}
/**
* Vendita: verifica l'inventario, paga sellPrice * quantity,
* aggiorna domanda/offerta e ricalcola subito i prezzi.
*/
export async function sellItem(
playerId: string,
itemId: string,
quantity: number,
): Promise<TradeResult> {
const m = balance.market;
return prisma.$transaction(async (tx) => {
const player = await getPlayerOrThrow(playerId, tx);
const price = await tx.marketPrice.findUnique({
where: { cityId_itemId: { cityId: player.currentCityId, itemId } },
include: { item: true, city: true },
});
if (!price) throw errors.notFound('Oggetto non vendibile in questo mercato');
const inventory = await tx.inventoryItem.findUnique({
where: { playerId_itemId: { playerId, itemId } },
});
if (!inventory || inventory.quantity < quantity) throw errors.insufficientItems();
const total = price.sellPrice * quantity;
await tx.player.update({
where: { id: player.id },
data: { money: { increment: total } },
});
if (inventory.quantity === quantity) {
await tx.inventoryItem.delete({ where: { playerId_itemId: { playerId, itemId } } });
} else {
await tx.inventoryItem.update({
where: { playerId_itemId: { playerId, itemId } },
data: { quantity: { decrement: quantity } },
});
}
const demand = clampDemandSupply(price.demand + quantity * m.demandDeltaPerSellUnit);
const supply = clampDemandSupply(price.supply + quantity * m.supplyDeltaPerSellUnit);
const events = await getActiveEvents(tx);
const next = computePrices(
price.item.basePrice,
price.city.economyModifier,
demand,
supply,
priceModifierForCity(events, price.cityId),
);
await tx.marketPrice.update({
where: { cityId_itemId: { cityId: price.cityId, itemId } },
data: { demand, supply, buyPrice: next.buyPrice, sellPrice: next.sellPrice },
});
return {
itemId,
itemName: price.item.name,
quantity,
unitPrice: price.sellPrice,
total,
playerMoney: player.money + total,
};
});
}