server: game API modules — market, missions, players, inventory, leaderboard, cities, events
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>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user