en-zombies is the undead population. A server-side director keeps a shared crowd of zombies around every player. The crowd is not per player, so two survivors standing together share one crowd. Zombies spawn out of sight and despawn when nobody is near.
Three types ship: walkers (slow, drunk gait), runners (fast but weaker) and brutes (+850 HP, no ragdoll, heavy knockdowns). Zombies see and hear, chase, search your last known position, then go back to wandering. Their hits can knock you down and infect you. They attack cars, smash windows and can pull drivers out. Hot zones raise density, change the type mix and can apply radiation. Safe zones spawn nothing, remove zombies that wander in and end chases. Dead zombies stay as corpses you can search for loot, split with nearby party members, and nearby zombies have positional audio.
At a glance
| Depends on | /onesync, en-core, en-ui |
| Optional | en-inventory (without it, loot pays cash only), en-target (without it, an E prompt is shown), en-party, en-weathersync (night multiplier via GlobalState.gameHour) |
| Start after | en-core, en-ui |
| Database tables | None. Zones placed in game are stored by en-core's content system |
| Config files | config/shared.lua (sent to clients), config/server.lua (server only) |
| Key binds | en_zombies_search, default E (prompt fallback only) |
GTA's ambient peds and traffic are switched off by en-core, not by en-zombies.
How it works
Authority
The server decides where zombies exist and validates every hit: the attacking ped's owner, range (plus 1.75 m tolerance), cooldown, that the target is not in a vehicle or a safe zone, and that the zombie is alive. The client that owns a zombie ped runs its AI through the states wander, investigate, chase and search.
Spawning
Every tick, the director walks through players who are in routing bucket 0, loaded and alive, in random order. For each one it works out a target count within 130 m: perPlayer times the zone density, times 0.4 in a vehicle and 1.4 at night. The player's client proposes ground points 65-115 m away, and the server rejects points near any player or within 30 m of a safe zone. The budget is 4 spawns per tick and 120 zombies in total. Nothing spawns within 40 m of a safe zone.
Models missing from the server's game build are detected (a client validates the list) and skipped with a warning.
Senses
Sight depends on day or night, weather, whether you are crouching, driving or using a flashlight, and a field of view. Hearing reacts to noises: gunshots, suppressed shots, explosions, sprinting, engines, horns and crashes. Each noise kind has a radius and a per-player throttle.
Infection
When a zombie hits you, the infection chance is its type's infectChance, reduced by armour (1 - armour/100 x 0.7) and further by the infectionResist skill bonus (capped at 0.9). An infected player gets AddInfection from en-core with a random amount from the type's infection range.
Radiation
Entering a zone with radiation above 0 sets the player's exposure with en-core's SetRadiationExposure. en-core absorbs it on each survival tick. Exposure is cleared when you leave the zone or the resource stops. See Survival system and Radiation zones.
Corpse loot and XP
Bodies stay for 120 seconds (20 seconds once looted). A search takes 3.5 seconds and only one player can search a body at a time. It rolls once on the type's loot table; the extraLoot skill bonus gives a chance of a second roll. With party members nearby, cash splits evenly (the remainder goes to the searcher) and item units are dealt out in turn; anyone who can't carry passes their share on. A search gives 5 XP.
Kills give XP by type (walker 10, runner 15, brute 40), up to 40 rewarded kills per minute per player, shared through encore.party.shareXP.
Configuration
config/shared.lua
| Key | Default | What it does |
|---|---|---|
Config.debug | false | Debug logging |
Config.world.density | 1.0 | Open-world density |
Config.world.types | { walker = 72, runner = 20, brute = 8 } | Weighted type mix outside zones |
Config.zones | burton_checkpoint (safe), sandy_quarantine (hot), humane_labs (hot, radiation 12) | Zone list |
Config.blips | zones = true, hot = { colour = 1, alpha = 80 }, safe = { colour = 2, alpha = 60 } | Radius blips for zones |
Config.types.<type> | walker, runner, brute | Per-type stats (see below) |
Config.appearance | damagePacks = {...}, packsPerZombie = 2 | Blood and burn decals, picked from the zVariant seed |
Config.senses | sightDay 55, sightNight 24, fieldOfView 140, chaseMemory 7000, searchTime 14000, searchRadius 14, alertRadius 16 | Sight and pursuit |
Config.noise | memory 12000; kinds gunshot, suppressed, explosion, sprint, vehicle, horn, crash | Hearing |
Config.vehicles | attack true, escapeSpeed 4.5, engineDamage 40, smashWindows true, pullOut true, pullOutChance 10 | Zombies versus vehicles |
Config.ai | tick 120, perTick 6, retask 3500 | Round-robin AI passes |
Config.loot | enabled true, searchTime 3500, distance 2.2, useTarget true, key 'E' | Body searching |
Config.audio | enabled true, maxVoices 6, range 38, volume 0.55, updateInterval 100 | Positional audio |
Config.senses also holds sight multipliers: crouchSight 0.5, vehicleSight 1.5, flashlightSight 2.2, and weather multipliers (FOGGY 0.45, BLIZZARD 0.5, THUNDER and SMOG 0.7, RAIN and SNOWLIGHT 0.8, CLEARING 0.9).
Noise kinds (radius in metres / throttle in ms per player):
| Kind | Radius | Throttle |
|---|---|---|
gunshot | 140 | 350 |
suppressed | 22 | 500 |
explosion | 280 | 0 |
sprint | 20 | 1500 |
vehicle | 55 | 2000 |
horn | 95 | 1000 |
crash | 110 | 1500 |
A vehicle counts as noisy above vehicleSpeed (12 m/s), and a crash is a speed drop of crashSpeedDrop (8 m/s).
Zombie types
Each entry in Config.types has: label, models, health (added to base 100), speed (1 walk, 2 jog, 3 sprint), clipset, damage, attackRange, attackCooldown (ms), knockdownChance (%), infectChance (%), infection = { min, max }, sight, hearing, ragdoll, sound (.ogg) and loot ('common' or 'rare').
| Type | Health | Damage | Notes |
|---|---|---|---|
| walker | 160 | 9 | infect chance 22% |
| runner | 110 | 6 | speed 3.0 |
| brute | 850 | 24 | knockdown 65%, loot rare |
config/server.lua
| Key | Default | What it does |
|---|---|---|
population.enabled | true | Director on or off at start |
population.maxTotal | 120 | Zombies alive server-wide |
population.perPlayer | 12 | Target per player before multipliers |
population.countRadius | 130.0 | Radius used to count nearby zombies |
population.spawnMin / spawnMax | 65 / 115 | Spawn distance band |
population.despawnDistance | 230 | Despawn when nobody is this close |
population.spawnsPerTick / tickInterval | 4 / 2000 ms | Spawn budget |
population.vehicleMultiplier | 0.4 | Density while in a vehicle |
population.corpseTime / lootedCorpseTime | 120 / 20 s | Corpse lifetime |
population.nightMultiplier / nightHours | 1.4 / { from = 20, to = 6 } | Needs GlobalState.gameHour |
infection.enabled / armorReduction | true / 0.7 | Bite infection |
xp.enabled | true | Kill and search XP |
xp.kill / killDefault | { walker = 10, runner = 15, brute = 40 } / 10 | XP per kill |
xp.search / killsPerMinute | 5 / 40 | Search XP and kill cap |
loot.common | rolls 2, nothing 45 | scrapmetal, garbage, bandage, water, burger, lockpick, cash 4-35 |
loot.rare | rolls 3, nothing 20 | scrapmetal, bandage, water, lockpick, radio, armour, cash 20-120 |
A loot entry is { item = 'name', min, max, weight } or { cash = true, min, max, weight }. nothing is the weight of an empty roll.
-- config/server.lua: a richer common table
loot = {
common = {
rolls = 2, nothing = 40,
{ item = 'bandage', min = 1, max = 2, weight = 20 },
{ item = 'water', min = 1, max = 1, weight = 15 },
{ cash = true, min = 5, max = 40, weight = 10 },
},
},Zones
{ name = 'my_zone', label = 'Label', type = 'hot', coords = vec3(x, y, z), radius = 150.0,
density = 1.5, types = { walker = 50, runner = 40, brute = 10 }, radiation = 0 }typeis'hot'or'safe'.- Zones are cylinders: only 2D distance counts.
- Values are clamped: radius 5-5000, density 0-10, radiation 0-1000. Unknown type names are ignored.
- Where zones overlap, safe zones win, then the smallest zone.
- Zones placed in game with the en-admin Build tab (content kind
'zone') override a config zone with the same name. Deleting the placement restores the config version.
Exports
Server
| Export | Arguments | Returns |
|---|---|---|
SpawnZombie | coords, typeName? (default 'walker') | Network id or nil. Yields, so call it from a thread |
ClearZombies | coords?, radius? (default 50) | Number removed. No coords = everywhere |
GetZombieCount | Number alive | |
IsZombie | entity | boolean |
SetDirectorEnabled / IsDirectorEnabled | enabled / | Pause or resume automatic spawning |
MakeNoise | coords, kind | Draws zombies to a point; kind is a Config.noise kind |
GetZoneAt | coords | Normalised zone table or nil |
BuilderList, BuilderGet, BuilderValidate, BuilderSchema, BuilderTemplate | kind, ... | en-admin Build tab integration for kind 'zone' |
Client
| Export | Arguments | Returns |
|---|---|---|
IsZombie | entity | boolean |
GetZombieType | entity | Type name or nil |
EmitNoise | kind | Noise at the local player's position (throttled; the server caps the radius) |
Commands
/zombies is restricted to group.admin.
| Command | What it does |
|---|---|
/zombies spawn <type> [count] | Up to 25 zombies, 8-20 m around you. Refused within 20 m of a safe zone |
/zombies clear [radius|all] | Default all. A radius needs an in-game admin |
/zombies stats | Alive and max, bodies, director state, per-type counts |
/zombies director <on|off> | Existing zombies stay when turned off |
Events
Server (local, for other resources)
| Event | Payload |
|---|---|
en-zombies:server:zombieSpawned | netId, typeName, zoneName |
en-zombies:server:zombieKilled | killerSrc|nil, typeName, netId. Fired when the director confirms the death |
en-zombies:server:playerBitten | targetSrc, typeName, infected |
en-zombies:server:corpseLooted | src, typeName, given, shares |
en-zombies:server:zoneChanged | src, zoneName|nil, previousName|nil |
Client (local)
| Event | Payload |
|---|---|
encore:client:zoneChanged | zone ({ name, label, type, radiation } or nil), previous. en-hud listens to this |
en-zombies:client:wasBitten | infected (boolean) |
State bags
Replicated entity state on each zombie:
| Key | Value |
|---|---|
zombie | Type name |
zVariant | Appearance seed, 1-9973 |
zHealth | Health applied |
zDead | Set by the owning client when it dies |
zLooted | Body has been searched |
With en-target running, bodies get the global ped option en-zombies:search ("Search body"). Without it, or with Config.loot.useTarget = false, an E prompt is used.
Examples
An alarm that draws a horde, and a bonus for killing brutes:
-- server
RegisterNetEvent('my-heist:alarm', function(coords)
exports['en-zombies']:MakeNoise(coords, 'explosion')
CreateThread(function()
for i = 1, 5 do
local offset = vec3(math.random(-20, 20) + 0.0, math.random(-20, 20) + 0.0, 0.0)
exports['en-zombies']:SpawnZombie(coords + offset, 'runner')
end
end)
end)
AddEventHandler('en-zombies:server:zombieKilled', function(killer, zombieType, netId)
if killer and zombieType == 'brute' then
encore.inventory.addItem(killer, 'scrapmetal', 2)
end
end)A new hot zone in config/shared.lua:
{
name = 'fort_zancudo', label = 'Fort Zancudo', type = 'hot',
coords = vec3(-2250.0, 3150.0, 32.8), radius = 600.0,
density = 2.2, types = { walker = 40, runner = 40, brute = 20 }, radiation = 0,
},