NPC Sales

Server-side NPC sales simulation for passive vending revenue in Oxide Vending.

How vending machines earn money on their own, and how to tune it.

How it works

Every so often (once a minute by default), the server gives each machine a chance to make a "pretend" sale, as if a pedestrian walked up and bought something. No actual pedestrian is involved in the money side — the whole thing is calculated on the server, so it costs almost nothing in performance and works even in empty areas of the map.

Each round, the server:

  1. skips machines that are broken, switched off, or empty
  2. works out each remaining machine's chance to sell (see below)
  3. rolls the dice for each machine
  4. for winners: picks an item, takes it from stock, adds the money to the machine, records the sale, applies a little wear, and awards the business XP

Default timing in config/npc_sales.lua:

  • TickInterval = 60 — one round every 60 seconds
  • BaseChancePerTick = 0.05 — every machine starts at a 5% chance per round, before the factors below

What decides the sale chance

The base chance is multiplied by six factors:

chance = base × time of day × location × machine type × pricing × stock variety × competition
FactorWhat drives it
Time of dayThe hour multipliers in the config — lunch rush is busy, night is dead
LocationIs the machine inside a configured busy or quiet area ("hotzone")?
Machine typeDrinks sell fast, electronics sell slowly
PricingCheap machines attract more buyers, overpriced ones fewer
Stock varietyA fuller machine with more different items sells better
CompetitionNearby machines of the same type steal each other's customers

Each factor is explained below.

Time of day

If oxide-weather is installed, the in-game hour is used. Otherwise the real-world server time is used. Either way the hour is looked up in TimeMultipliers.

Examples from the shipped config:

  • midnight → 0.2 (very slow)
  • 8 AM → 1.3 (morning rush)
  • noon → 1.5 (lunch rush, the daily peak)
  • 5 PM → 1.4 (evening commute)
  • 11 PM → 0.3 (winding down)

Location (hotzones)

Hotzones are circles on the map with their own traffic multiplier, in Config.NPCSales.Hotzones.

The shipped config makes busy city spots sell much better — Legion Square (2.5x), Del Perro Pier, the airport, Vespucci Beach (around 2x) — and rural spots much worse: Sandy Shores, Paleto Bay, Grapeseed, Harmony, and Chumash sit between 0.3x and 0.5x.

A machine outside every hotzone uses DefaultLocationMultiplier (1.0 — average).

Adding your own zone

{
    name = 'custom_zone',
    coords = vector3(x, y, z),   -- center of the area
    radius = 150.0,              -- size in meters
    multiplier = 1.5,            -- 1.5 = 50% more sales here
}

Machine type

From MachineTypeModifiers:

TypeMultiplier
drinks1.5
snacks1.3
general0.8
electronics0.4

This is part of the game balance: cheap fast-moving goods sell constantly, expensive electronics rarely — but for much more money per sale.

Pricing

The simulation looks at how the machine's average price compares to the items' base prices:

  • selling at base price or below → strong bonus (1.5x)
  • the default 1.5x markup → still favorable (about 1.25x)
  • double base price → neutral
  • triple base price → heavy penalty (0.5x)

(For the curious, the formula is 1.5 - ((average markup - 1.0) × 0.5), clamped between 0.5 and 1.5.)

Stock variety

The multiplier is simply filled slots ÷ total slots. A machine with all 8 slots stocked sells at full speed; a machine with 2 of 8 slots stocked sells at a quarter speed. Keeping machines full and varied matters.

Competition

When machines of the same type sit within Config.Competition.Radius (500 meters by default) of each other, they compete:

  • only same-type machines compete (a snack machine doesn't hurt a drinks machine)
  • a business's own machines never compete with each other (unless you flip SameOwnerCompetes)
  • the cheapest competitor gets an extra bonus (CheapestBonus = 1.5)
  • the result is clamped between MinMultiplier (0.3) and MaxMultiplier (1.8)

The dashboard's competition page shows owners who they're up against and suggests a price slightly below the local average (95% of it, but never below 85% of base price). That analysis is recalculated at most every 5 minutes (AnalysisCacheTime).

What gets bought

When a machine wins its roll, the simulation picks an item from its stock, weighted so that cheaper items and well-stocked items are picked more often. It then buys 1 or 2 of them (MinQuantity / MaxQuantity).

With AvoidLastItem = true (the default), NPCs never take a machine's last copy of an item — real players always find something left to buy.

What a sale does

Each NPC sale:

  • removes the items from stock
  • adds the money to the machine's takings (after RevenueMultiplier and the business's level-based NPC bonus)
  • records a transaction of type npc_sale (shown with "NPC" as the buyer in the history)
  • wears the machine down slightly (less than a player sale: 0.3 vs 0.5)
  • awards the business XP: 5 base, plus 0.1 per dollar of the sale
  • counts toward the business's sales milestones

The walking-pedestrian animations

When players are near a machine that makes an NPC sale, the resource may grab a real nearby pedestrian and play a "using the machine" scene. This is purely visual:

VisualFeedback = {
    Enabled = true,
    PlayerRange = 50.0,        -- a player must be this close for the scene to play
    PedSearchRadius = 30.0,    -- how far around the machine to look for a pedestrian
    MachineCooldown = 30000,   -- at most one scene per machine per 30 seconds
}

Turning this off does not reduce the income — it only removes the visual flavor.

Admin tools

  • /vendingnpc toggle — turn the simulation on/off until restart
  • /vendingnpc stats — sales and revenue since the last restart
  • /vendingnpc force — run a boosted test round right now

For developers

local stats = exports['oxide-vending']:GetNPCSalesStats()
-- stats.totalSales, stats.totalRevenue, stats.simulationActive,
-- stats.lastTickTime, stats.tickInterval

Performance notes

  • everything runs server-side; no entities are spawned for the sales themselves
  • each round is a simple pass over the machine list with distance checks
  • the dashboard's competition analysis is cached, so opening it repeatedly is cheap