Shops and player trade

Both kinds of trade use the game's shop screen. In both, the screen only estimates the deal. The server recalculates it from its own data when the player confirms.

NPC shops

kcds.characters.Vendor is a shop stored on the server. It has a shelf of priced rows, a list of what it pays for players' items, and a purse. Prices are counted in the game's money item.

const { Vendor } = kcds.characters;
const { Events } = kcds.core;

const smith = Vendor.create({ name: 'Smith', purse: 5000 });

Vendor.setShelf(smith, [
  { item: 'arrow_crude', amount: 50, price: 12 },
  { item: 'bread', amount: 10, price: 30 },
]);
Vendor.setOffers(smith, [{ item: 'arrow_crude', price: 6 }]);

Events.on('npcInteract', (npc, player) => {
  if (npc.name === 'Smith') {
    Vendor.open(smith, player, npc);
  }
});

Events.on('vendorTrade', (vendor, player, bought, sold, balance) => {
  console.info(`${player.nickname} traded, balance ${balance}`);
});

Setting up

  • Vendor.create({ name?, purse?, buys? }): with no purse, the money never runs out. With buys: false, the shop only sells.
  • setShelf(vendor, rows): up to 128 rows of { item, amount, price, metadata? }.
  • setOffers(vendor, rows): what the shop pays for items.
  • fillPurse and purseOf: set and read its money.

Running a session

Vendor.open(vendor, player, npc?) returns a session id. If you pass an npc, the shop closes when that NPC dies or disappears. Also available: Vendor.close(session), activeSession(player) and shopOf(session).

Refused trades

The server refuses a basket if it was priced against an older shelf, if either side cannot pay, or if the goods do not fit in the player's inventory. Items players sell do not appear on the shelf.

When a shop closes

The event vendorClosed(vendor, player, reason) gives one of these reasons:

ReasonMeaning
0the player closed it
1the server closed it
2another shop replaced it
3the screen could not open
4the player left

Reference: Vendor.

Trade between players

kcds.players.Trade shows one player's inventory as a shop on another player's screen. The buyer sees the seller's items and the seller's money as the purse, but not the seller's money items, quest items or worn gear. Each basket is checked against both inventories, then both change together or neither does.

const { Events, Chat } = kcds.core;
const { Player, Trade } = kcds.players;

// "/sell Hans" shows your inventory as a shop on Hans's screen.
Events.on('playerCommand', (seller, command, args) => {
  if (command !== 'sell') {
    return;
  }
  const buyer = Player.all().find((player) => player.nickname === args[0]);
  if (!buyer) {
    return;
  }
  const session = Trade.open(seller, buyer, {
    prices: { apple: 7 },
    buys: true, // Hans can also sell to the seller
    buyPrices: { apple: 3 },
  });
  if (session === 0) {
    Chat.sendToPlayer(seller, 'The trade could not open.');
  }
});

// Return false to refuse a basket; here, no more than 10 of anything at once.
Events.on('tradeItem', (seller, buyer, bought, sold, balance) => {
  return bought.every((line) => line.amount <= 10);
});

Events.on('tradeDone', (seller, buyer, bought, sold, balance) => {
  console.info(`${buyer.nickname} paid ${-balance} to ${seller.nickname}`);
});

In tradeItem and tradeDone, bought and sold are lists of { item, name, amount, price, metadata }. balance is the money the buyer receives, and a negative number is what they pay.

Opening: Trade.open(seller, buyer, options?) returns the session id. It returns 0 if a player is gone or not yet in the world, if both are the same player, or if a tradeOpen handler returned false.

Prices: prices and buyPrices name items the same way as Items and take whole money units. Items without a price use the server's price for their quality, from the item config or the game.

Changes while open: if either inventory changes, the screen updates, and a basket made on the old screen is refused.

Ending: a trade ends when any of these happens:

  • the buyer closes the screen
  • either player leaves or is knocked out
  • a script calls Trade.close(session)
  • the resource that opened it stops

tradeClosed(seller, buyer, reason) then fires with the same reasons as vendorClosed.

Trade.activeSession(player) gives the trade a player is in as buyer or seller, or 0.

Reference: Trade.