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 { 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 { 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, }; }); }