fb62a39dab
Add routes and services for all game domains: market buy/sell with dynamic pricing, mission generation and completion, player stats and travel, inventory queries, leaderboard, city list, and world events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|