EndCore Framework

Add a trader

Add an NPC trader in config or place one live with the en-admin Build tab, set what they sell and buy, and understand their prices.

Traders are how scavenged junk turns into supplies. Each one is an NPC with limited stock, prices that rise as the shelf empties, and a list of things they'll buy. You can add a trader in two ways:

  • In config, in en-shops/config/shared.lua. Good for the permanent traders your server is built around.
  • In game, from the Build tab of the admin panel. Good for placing and adjusting traders live, without a restart.

Option 1: add a trader in config

Open resources/[encore]/en-shops/config/shared.lua and add an entry to Config.traders:

lua
Config.traders[#Config.traders + 1] = {
    id = 'vinewood_fence',
    label = 'Vinewood Fence',
    name = 'Marta',
    greeting = { 'Quick. What have you got?', 'No questions, no refunds.' },
    ped = {
        model = 'a_f_y_hipster_02',
        coords = vec4(300.2, 180.4, 104.1, 70.0),
        scenario = 'WORLD_HUMAN_SMOKING',
    },
    blip = { sprite = 52, colour = 47 },
    sells = {
        { item = 'lockpick', max = 10 },
        { item = 'ammo_rifle', max = 120, barter = { { item = 'scrapmetal', count = 2 } }, requires = { level = 6 } },
        { item = 'gps', max = 2, barterOnly = true, barter = { { item = 'radio', count = 1 }, { item = 'duct_tape', count = 2 } } },
    },
    buys = { categories = { 'weapon', 'tool' }, items = { 'radio' }, rate = 0.5 },
}

Use /coords from en-admin to copy your current position as a vec4 while standing where the trader should stand.

Trader fields

FieldWhat it does
idUnique id. A trader placed in game with the same id replaces this one.
labelStall name shown on the trade page and blip
nameThe trader's name
greetingA line, or a list of lines picked at random when players talk to them
ped.modelPed model
ped.coordsvec4 position and heading
ped.scenarioOptional idle scenario
blip{ sprite, colour }, or false for no map blip
sellsOffers, see below
buysWhat they accept, see below

What a trader sells

Each entry in sells:

FieldWhat it does
itemItem name
maxShelf capacity. Stock starts full.
priceOptional fixed base price. Defaults to the item's value in Config.values.
barterItems taken per unit bought, as { { item, count }, ... }
barterOnlytrue means no cash option. Needs barter.
requires{ level = n } for character level, or { skill = 'crafting', level = n } for a skill level

What a trader buys

FieldWhat it does
categoriesItem categories they accept, such as food, material, weapon
itemsSpecific item names they accept
rateShare of an item's value they pay, overriding Config.pricing.sellRate. Clamped 0 to 2.

Give every item a value

Config.values in the same file is the base cash value of each item. It is the default buy price, and it is what traders pay a share of when buying.

lua
Config.values = {
    -- ...
    lockpick = 35,
    radio = 150,
    geiger_counter = 300,
}
Warning

An item with no value can't be sold to anyone, even if a trader's buys list includes it.

Restart en-shops (or the server) to load the new trader.

Option 2: place a trader in game

  1. Press F10 to open the admin panel and go to the Build tab.
  2. Choose Traders. The list shows config traders and placed ones, with distance and a button to teleport to each.
  3. Choose New. A form opens with defaults filled in.
  4. Fill in Shop name and Trader name, and add greetings, one per line.
  5. Enter a Ped model and, optionally, a Scenario.
  6. Stand where the trader should be and press Use my position on the Position field. It records your heading too, and places the ped at your feet.
  7. Add Sells offers: item, max stock, optional price, barter cost, barter only, required level or skill.
  8. Fill in Buys categories, Buys items and Buy rate.
  9. Save. en-shops validates the data, and the trader appears for everyone straight away.

To change a config trader live, open it from the list, edit and save. The placed version overrides the config one with the same id. Delete the placed entry to go back to the config version.

Placed traders are stored in the encore_content table and survive restarts. See en-admin and Content registry.

How prices work

Prices are decided on the server, from stock and holdings.

Buying from a trader. The price rises as the shelf empties, by up to Config.pricing.scarcity (50%):

text
price = max(1, round(base * (1 + (1 - stock / max) * scarcity)))

A lockpick worth 35 with 5 of 10 left costs round(35 * 1.25) = 44.

Selling to a trader. They pay sellRate (45%) of the value, less the more of that item they already hold, down by up to maxDrop (60%) at saturation (25) units:

text
price = floor(value * rate * (1 - min(maxDrop, holdings / saturation * maxDrop)))

A radio worth 150 sells for 67 to a trader holding none, and for 51 once they hold 10.

Restocking. Every Config.restock.minutes (30), each shelf refills by 25% of its max, and 30% of holdings are cleared. Selling an item the trader also sells puts it back on their shelf.

KeyDefaultWhat it does
Config.pricing.scarcity0.5Empty shelf charges up to +50%
Config.pricing.sellRate0.45Share of value paid when buying from players
Config.pricing.saturation25Holdings at which the price drop is at its largest
Config.pricing.maxDrop0.6Largest reduction of the sell price
Config.pricing.maxBatch50Most units per trade
Config.restock.minutes30Minutes between restocks
Config.restock.refill0.25Share of max refilled per restock
Config.restock.clearance0.3Share of holdings cleared per restock

Admins can refill every shelf and clear all holdings with /restockshops.

Scripting around traders

en-shops fires server events you can hook into, and exports current prices.

lua
-- server: a small XP reward for bartering instead of paying cash
AddEventHandler('en-shops:server:bought', function(source, traderId, item, amount, method)
    if method == 'barter' then
        exports['en-core']:AddXP(source, 2 * amount, 'Bartered with a trader')
    end
end)

-- server: log big sales
AddEventHandler('en-shops:server:sold', function(source, traderId, item, amount, totalCash)
    if totalCash >= 1000 then
        exports['en-core']:Log('default', ('%s sold %dx %s to %s for $%d'):format(
            exports['en-core']:GetCharacterName(source), amount, item, traderId, totalCash))
    end
end)

-- server: read a live price
local price = exports['en-shops']:GetBuyPrice('vinewood_fence', 'lockpick')