NPC Sales

Server-side NPC sales simulation for passive income generation in Oxide Vending.

Overview

The NPC sales system simulates customer purchases at vending machines without spawning actual NPCs. This provides passive income for business owners based on machine location, time of day, pricing, and stock availability.


How It Works

Simulation Loop

Every tick (default: 60 seconds), the server evaluates each active machine:

  1. Check if machine is active status
  2. Check if machine has any stock
  3. Calculate sale chance based on multipliers
  4. Roll for sale (random < chance)
  5. If successful, process the sale

Sale Chance Formula

Sale Chance = BaseChance × Time × Location × Type × Price × Stock × Competition
FactorDescriptionRange
BaseChanceConfig value0.05 (5%)
TimeHour of day0.1 - 1.5
LocationHotzone proximity0.3 - 2.5
TypeMachine category0.4 - 1.5
PriceMarkup competitiveness0.5 - 1.5
StockVariety multiplier0.0 - 1.0
CompetitionNearby competition0.3 - 1.8

Example Calculation

A drinks machine at Legion Square during lunch hour with competitive pricing:

0.05 × 1.5 × 2.5 × 1.5 × 1.25 × 0.8 × 1.0 = 0.28 (28% chance)

Time Multipliers

Sales frequency varies by GTA in-game hour:

HourMultiplierDescription
0-40.1 - 0.2Late night (dead)
5-60.3 - 0.6Early morning
7-91.0 - 1.3Morning commute (rush)
10-111.0 - 1.1Mid-morning
12-131.4 - 1.5Lunch rush (peak)
14-161.0 - 1.2Afternoon
17-181.3 - 1.4Evening commute
19-210.6 - 1.1Evening
22-230.3 - 0.4Night

Time Source: Uses any resource providing a getTime() export (e.g. qb-weathersync) for in-game time. Falls back to real time if no time source is available.


Location Hotzones

Predefined areas with modified sale frequency:

High Traffic (2.0x - 2.5x)

ZoneCoordsRadiusMultiplier
Legion Square(195, -935, 30)150m2.5x
Del Perro Pier(-1650, -1100, 13)200m2.0x
Airport Terminals(-1037, -2737, 20)400m2.0x
Vespucci Beach(-1350, -1150, 4)250m1.9x

Medium-High Traffic (1.5x - 1.8x)

ZoneCoordsRadiusMultiplier
Vinewood Blvd(320, 180, 103)200m1.8x
Rockford Plaza(-250, -330, 30)150m1.7x
Pillbox Hospital(360, -585, 28)100m1.6x
Mission Row PD(428, -984, 30)80m1.5x
Maze Bank Tower(-75, -818, 326)100m1.5x

Medium Traffic (1.2x - 1.4x)

ZoneCoordsRadiusMultiplier
Little Seoul(-750, -750, 28)200m1.4x
Strawberry(250, -1250, 29)200m1.3x
Mirror Park(1050, -450, 65)200m1.2x

Low Traffic (0.3x - 0.5x)

ZoneCoordsRadiusMultiplier
Sandy Shores(1890, 3780, 33)300m0.5x
Chumash(-3150, 1100, 20)200m0.5x
Paleto Bay(-165, 6320, 31)200m0.4x
Harmony(550, 2670, 42)150m0.4x
Grapeseed(1700, 4800, 42)200m0.3x

Machines outside any hotzone use the default multiplier (1.0x).

Adding Custom Hotzones

Add to Config.NPCSales.Hotzones in config/npc_sales.lua:

{
    name = 'custom_zone',
    coords = vector3(x, y, z),
    radius = 150.0,
    multiplier = 1.5,  -- >1 = busy, <1 = quiet
}

Machine Type Multipliers

Different machine types have inherent sale frequencies:

TypeMultiplierReasoning
drinks1.5xHigh velocity - everyone buys drinks
snacks1.3xFrequent impulse purchases
general0.8xOccasional purchases
electronics0.4xRare - expensive items

Price Competitiveness

Lower markups attract more NPC customers:

multiplier = 1.5 - ((avgMarkup - 1.0) * 0.5)
-- Clamped between 0.5 and 1.5
MarkupMultiplierDescription
0.8x1.5xBelow base price (bonus)
1.0x1.5xAt base price (bonus)
1.5x1.25xDefault markup
2.0x1.0xNeutral
3.0x0.5xMaximum markup (penalty)

Stock Availability

Machines with more item variety are more attractive:

multiplier = stockedSlots / totalSlots
-- 0.0 to 1.0

A drinks machine (8 slots) with 4 stocked items = 0.5x multiplier.


Competition System

When multiple machines of the same type are nearby, they compete for customers.

Competition Detection

  • Radius: 500m (configurable)
  • Only same machine type competes
  • Same-owner machines don't compete (configurable)

Competition Multiplier

The cheapest machine in the area gets a bonus, expensive machines get penalties:

PositionMultiplier
CheapestUp to 1.8x (bonus: 1.5x × competition factor)
Average~1.0x
Most expensiveDown to 0.3x

Suggested Pricing

The dashboard shows suggested prices based on competition:

  • Suggested price = 95% of competitor average
  • Never suggests below 85% of base price

Item Selection Algorithm

When a sale occurs, items are selected by weighted random:

weight = (10 / sqrt(price)) * sqrt(quantity)

This means:

  • Cheaper items are more likely to sell
  • Items with more stock are slightly preferred
  • Expensive items still sell occasionally

Purchase Behavior

SettingDefaultDescription
MinQuantity1Minimum items per purchase
MaxQuantity2Maximum items per purchase
AvoidLastItemtrueNPCs won't buy the last item in stock
PreferCheaperItemstrueWeight selection toward cheaper items

Transaction Logging

NPC sales are logged with:

transaction_type = 'npc_sale'
player_citizenid = 'NPC'

In the dashboard, NPC sales appear with:

  • Cyan styling
  • "NPC" label
  • Can be filtered separately from player sales

Progression Integration

NPC sales contribute to the progression system:

ActionXP Earned
NPC Sale5 + (0.1 × sale amount)

Revenue from NPC sales also counts toward:

  • Lifetime revenue tracking
  • Revenue milestones
  • Level-up requirements

NPC Revenue Boost

Higher-level businesses earn bonus revenue from NPC sales:

LevelBoost
10%
25%
38%
412%
515%
618%
722%
825%
928%
1035%

Admin Commands

CommandDescription
/vendingnpc toggleEnable/disable NPC simulation
/vendingnpc statsView NPC sales statistics
/vendingnpc forceForce a simulation tick

Export

local stats = exports['oxide-vending']:GetNPCSalesStats()
-- Returns: {
--     totalSales = number,
--     totalRevenue = number,
--     simulationActive = boolean,
--     lastTickTime = number,
--     tickInterval = number,
-- }

Configuration Reference

Config.NPCSales = {
    Enabled = true,
    TickInterval = 60,              -- Seconds between ticks
    BaseChancePerTick = 0.05,       -- 5% base chance
    MinQuantity = 1,
    MaxQuantity = 2,
    AvoidLastItem = true,
    PreferCheaperItems = true,
    RevenueMultiplier = 1.0,        -- Scale all NPC revenue

    MachineTypeModifiers = { ... },
    TimeMultipliers = { ... },
    Hotzones = { ... },

    DefaultLocationMultiplier = 1.0,
}

Config.Competition = {
    Enabled = true,
    Radius = 500.0,
    SameOwnerCompetes = false,
    MinMultiplier = 0.3,
    MaxMultiplier = 1.8,
    CheapestBonus = 1.5,
}

Visual Feedback

When players are nearby, NPC sales trigger visual animations on ambient pedestrians to simulate customers using the machines.

VisualFeedback = {
    Enabled = true,             -- Master toggle for NPC animations
    PlayerRange = 50.0,         -- Player must be within this distance to trigger
    PedSearchRadius = 30.0,     -- Search radius for ambient peds around machine
    MachineCooldown = 30000,    -- Cooldown per machine in ms (30 seconds)
},

This is purely cosmetic and doesn't affect the actual NPC sales calculations.


Performance Considerations

  • Simulation runs only on server
  • No NPCs are spawned (entity-free)
  • Machine iteration is O(n) per tick
  • Hotzone checking uses simple distance calculation
  • Competition analysis is cached for 5 minutes