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:
- Check if machine is
activestatus - Check if machine has any stock
- Calculate sale chance based on multipliers
- Roll for sale (random < chance)
- If successful, process the sale
Sale Chance Formula
Sale Chance = BaseChance × Time × Location × Type × Price × Stock × Competition
| Factor | Description | Range |
|---|---|---|
| BaseChance | Config value | 0.05 (5%) |
| Time | Hour of day | 0.1 - 1.5 |
| Location | Hotzone proximity | 0.3 - 2.5 |
| Type | Machine category | 0.4 - 1.5 |
| Price | Markup competitiveness | 0.5 - 1.5 |
| Stock | Variety multiplier | 0.0 - 1.0 |
| Competition | Nearby competition | 0.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:
| Hour | Multiplier | Description |
|---|---|---|
| 0-4 | 0.1 - 0.2 | Late night (dead) |
| 5-6 | 0.3 - 0.6 | Early morning |
| 7-9 | 1.0 - 1.3 | Morning commute (rush) |
| 10-11 | 1.0 - 1.1 | Mid-morning |
| 12-13 | 1.4 - 1.5 | Lunch rush (peak) |
| 14-16 | 1.0 - 1.2 | Afternoon |
| 17-18 | 1.3 - 1.4 | Evening commute |
| 19-21 | 0.6 - 1.1 | Evening |
| 22-23 | 0.3 - 0.4 | Night |
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)
| Zone | Coords | Radius | Multiplier |
|---|---|---|---|
| Legion Square | (195, -935, 30) | 150m | 2.5x |
| Del Perro Pier | (-1650, -1100, 13) | 200m | 2.0x |
| Airport Terminals | (-1037, -2737, 20) | 400m | 2.0x |
| Vespucci Beach | (-1350, -1150, 4) | 250m | 1.9x |
Medium-High Traffic (1.5x - 1.8x)
| Zone | Coords | Radius | Multiplier |
|---|---|---|---|
| Vinewood Blvd | (320, 180, 103) | 200m | 1.8x |
| Rockford Plaza | (-250, -330, 30) | 150m | 1.7x |
| Pillbox Hospital | (360, -585, 28) | 100m | 1.6x |
| Mission Row PD | (428, -984, 30) | 80m | 1.5x |
| Maze Bank Tower | (-75, -818, 326) | 100m | 1.5x |
Medium Traffic (1.2x - 1.4x)
| Zone | Coords | Radius | Multiplier |
|---|---|---|---|
| Little Seoul | (-750, -750, 28) | 200m | 1.4x |
| Strawberry | (250, -1250, 29) | 200m | 1.3x |
| Mirror Park | (1050, -450, 65) | 200m | 1.2x |
Low Traffic (0.3x - 0.5x)
| Zone | Coords | Radius | Multiplier |
|---|---|---|---|
| Sandy Shores | (1890, 3780, 33) | 300m | 0.5x |
| Chumash | (-3150, 1100, 20) | 200m | 0.5x |
| Paleto Bay | (-165, 6320, 31) | 200m | 0.4x |
| Harmony | (550, 2670, 42) | 150m | 0.4x |
| Grapeseed | (1700, 4800, 42) | 200m | 0.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:
| Type | Multiplier | Reasoning |
|---|---|---|
drinks | 1.5x | High velocity - everyone buys drinks |
snacks | 1.3x | Frequent impulse purchases |
general | 0.8x | Occasional purchases |
electronics | 0.4x | Rare - 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
| Markup | Multiplier | Description |
|---|---|---|
| 0.8x | 1.5x | Below base price (bonus) |
| 1.0x | 1.5x | At base price (bonus) |
| 1.5x | 1.25x | Default markup |
| 2.0x | 1.0x | Neutral |
| 3.0x | 0.5x | Maximum 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:
| Position | Multiplier |
|---|---|
| Cheapest | Up to 1.8x (bonus: 1.5x × competition factor) |
| Average | ~1.0x |
| Most expensive | Down 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
| Setting | Default | Description |
|---|---|---|
| MinQuantity | 1 | Minimum items per purchase |
| MaxQuantity | 2 | Maximum items per purchase |
| AvoidLastItem | true | NPCs won't buy the last item in stock |
| PreferCheaperItems | true | Weight 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:
| Action | XP Earned |
|---|---|
| NPC Sale | 5 + (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:
| Level | Boost |
|---|---|
| 1 | 0% |
| 2 | 5% |
| 3 | 8% |
| 4 | 12% |
| 5 | 15% |
| 6 | 18% |
| 7 | 22% |
| 8 | 25% |
| 9 | 28% |
| 10 | 35% |
Admin Commands
| Command | Description |
|---|---|
/vendingnpc toggle | Enable/disable NPC simulation |
/vendingnpc stats | View NPC sales statistics |
/vendingnpc force | Force 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