Ambulance Job System Overview
An ambulance job script is essential for any roleplay server that aims for immersive emergency services gameplay. The EMS system handles everything from receiving dispatch calls when a player goes down, to on-scene treatment mechanics, hospital transport, and medical billing. A well-designed ambulance script creates meaningful gameplay for EMS players while ensuring downed players are not stuck waiting indefinitely. The architecture splits into four major subsystems: the dispatch and notification system that alerts EMS to incidents, the treatment and stabilization mechanics that give medics actual tasks to perform, the hospital check-in and recovery process, and the billing system that creates an economic cost for injuries. Each subsystem needs to be independently functional so that if no EMS players are on duty, automatic fallback systems handle player recovery.
Job Configuration and Duty System
The ambulance job needs clearly defined grades with different capabilities at each level. Trainees might only be able to perform basic first aid, while senior paramedics can administer advanced treatments and access restricted medical supplies. Use a configuration table that maps job grades to available actions, required items, and payment rates. The duty system should track which EMS players are currently active so the server knows whether to use the automatic respawn fallback:
Config.AmbulanceJob = {
name = 'ambulance',
grades = {
[0] = {
label = 'Trainee',
treatments = {'bandage', 'painkillers'},
canRevive = false,
salary = 250,
},
[1] = {
label = 'EMT',
treatments = {'bandage', 'painkillers', 'splint', 'iv_drip'},
canRevive = true,
salary = 400,
},
[2] = {
label = 'Paramedic',
treatments = {'bandage', 'painkillers', 'splint', 'iv_drip',
'defibrillator', 'blood_transfusion'},
canRevive = true,
salary = 550,
},
[3] = {
label = 'Chief',
treatments = {'all'},
canRevive = true,
salary = 750,
},
},
minOnDutyForNoAutoRespawn = 2,
respawnTimer = 300, -- seconds before auto-respawn when no EMS
hospitalCoords = vector3(311.2, -584.3, 43.3),
bedSpawns = {
vector4(309.7, -583.8, 43.3, 70.0),
vector4(313.0, -585.2, 43.3, 70.0),
vector4(316.3, -586.6, 43.3, 70.0),
},
}
The minOnDutyForNoAutoRespawn setting controls when the server switches between EMS-dependent and automatic recovery. When fewer than two EMS are on duty, downed players get a timer-based respawn option after waiting the configured number of seconds. This prevents players from being stuck waiting for medical attention that will never arrive.
Dispatch and Notification System
When a player is incapacitated, the system broadcasts a dispatch alert to all on-duty EMS players showing the location, nature of the emergency, and a way to claim the call. Multiple EMS players can see the alert, but only one should claim it to avoid confusion. Implement a claim system where the first responder to accept the call gets it assigned, and other EMS see it as claimed with the responding unit's identifier:
-- Server: handle player down event
local ActiveCalls = {}
RegisterNetEvent('ambulance:server:playerDown', function(deathCause)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local ped = GetPlayerPed(src)
local coords = GetEntityCoords(ped)
local streetHash, _ = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
local streetName = GetStreetNameFromHashKey(streetHash)
local callId = 'EMS-' .. os.time() .. '-' .. src
ActiveCalls[callId] = {
source = src,
patient = Player.PlayerData.charinfo.firstname .. ' ' ..
Player.PlayerData.charinfo.lastname,
coords = coords,
street = streetName,
cause = deathCause,
time = os.time(),
claimed = false,
responder = nil,
}
-- Notify all on-duty EMS
local emsList = QBCore.Functions.GetPlayersOnDuty('ambulance')
for _, emsId in ipairs(emsList) do
TriggerClientEvent('ambulance:client:newCall', emsId, callId, ActiveCalls[callId])
end
end)
RegisterNetEvent('ambulance:server:claimCall', function(callId)
local src = source
local call = ActiveCalls[callId]
if not call or call.claimed then return end
local Player = QBCore.Functions.GetPlayer(src)
if not Player or Player.PlayerData.job.name ~= 'ambulance' then return end
call.claimed = true
call.responder = src
-- Notify all EMS that call is claimed
local emsList = QBCore.Functions.GetPlayersOnDuty('ambulance')
for _, emsId in ipairs(emsList) do
TriggerClientEvent('ambulance:client:callClaimed', emsId, callId, Player.PlayerData.charinfo.firstname)
end
-- Set GPS waypoint for responding EMT
TriggerClientEvent('ambulance:client:setWaypoint', src, call.coords)
end)
Include the death cause in the dispatch information so the responding EMT can prepare appropriate treatment items before arriving on scene. Common causes like gunshot wounds, vehicle collisions, falls, and drowning each require different treatment approaches, which adds depth to the EMS roleplay experience.
Treatment and Revive Mechanics
Treatment mechanics should be more involved than simply pressing a button to revive someone. Create a multi-step process where the EMT must first stabilize the patient with basic treatments before performing the actual revival. Each treatment step consumes an item from the EMT's inventory and takes a set amount of time with a progress bar animation. The patient's condition can be modeled with a simple health state that improves with each treatment applied:
-- Client: treatment system
local TreatmentSteps = {
['bandage'] = {
label = 'Apply Bandage',
duration = 5000,
anim = {dict = 'mini@cpr@char_a@cpr_str', name = 'cpr_pumpchest'},
healthRestore = 10,
item = 'bandage',
},
['painkillers'] = {
label = 'Administer Painkillers',
duration = 3000,
anim = {dict = 'mp_arresting', name = 'a_uncuff'},
healthRestore = 15,
item = 'painkillers',
},
['defibrillator'] = {
label = 'Use Defibrillator',
duration = 8000,
anim = {dict = 'mini@cpr@char_a@cpr_str', name = 'cpr_pumpchest'},
healthRestore = 0,
item = 'defibrillator',
canRevive = true,
},
}
function PerformTreatment(targetId, treatmentType)
local treatment = TreatmentSteps[treatmentType]
if not treatment then return end
-- Check if EMT has required item
if not HasItem(treatment.item) then
QBCore.Functions.Notify('Missing: ' .. treatment.label, 'error')
return
end
-- Play animation and progress bar
TaskPlayAnim(PlayerPedId(), treatment.anim.dict, treatment.anim.name,
8.0, -8.0, treatment.duration, 1, 0, false, false, false)
QBCore.Functions.Progressbar('treatment_' .. treatmentType,
treatment.label, treatment.duration, false, true, {}, {}, {}, {},
function() -- success
TriggerServerEvent('ambulance:server:applyTreatment',
targetId, treatmentType)
ClearPedTasks(PlayerPedId())
end,
function() -- cancel
ClearPedTasks(PlayerPedId())
QBCore.Functions.Notify('Treatment cancelled', 'error')
end
)
end
The progress bar with animation creates a realistic treatment scene that other players can observe, enhancing the roleplay atmosphere. Make the defibrillator the final step that actually performs the revival, requiring the EMT to first stabilize the patient with bandages and painkillers. This multi-step approach makes the EMS role more engaging than a simple one-click revive.
Hospital Check-In and Recovery
After reviving a patient in the field or transporting them to the hospital, an EMS player can check them into a hospital bed for full recovery. The check-in process heals the patient completely, charges a medical bill, and logs the visit. Hospital beds should be tracked to prevent multiple patients from being assigned to the same bed. Create a bed management system that marks beds as occupied and releases them after a configurable recovery time:
-- Server: hospital bed management
local OccupiedBeds = {}
RegisterNetEvent('ambulance:server:checkInPatient', function(patientId)
local src = source
local EMT = QBCore.Functions.GetPlayer(src)
local Patient = QBCore.Functions.GetPlayer(patientId)
if not EMT or not Patient then return end
if EMT.PlayerData.job.name ~= 'ambulance' then return end
-- Find available bed
local bedIndex = nil
for i, bed in ipairs(Config.AmbulanceJob.bedSpawns) do
if not OccupiedBeds[i] then
bedIndex = i
break
end
end
if not bedIndex then
TriggerClientEvent('QBCore:Notify', src, 'No beds available', 'error')
return
end
OccupiedBeds[bedIndex] = patientId
-- Calculate and charge medical bill
local bill = CalculateMedicalBill(Patient)
Patient.Functions.RemoveMoney('bank', bill, 'medical-bill')
-- Heal patient and teleport to bed
local bed = Config.AmbulanceJob.bedSpawns[bedIndex]
TriggerClientEvent('ambulance:client:bedRecovery', patientId, bed, bill)
-- Pay EMT for service
local salary = Config.AmbulanceJob.grades[EMT.PlayerData.job.grade.level].salary
EMT.Functions.AddMoney('bank', salary, 'ems-treatment-pay')
-- Release bed after recovery
SetTimeout(60000, function()
OccupiedBeds[bedIndex] = nil
end)
end)
function CalculateMedicalBill(Patient)
local baseCost = 500
local injuryMultiplier = 1.0
-- Could factor in injury severity, treatment count, etc.
return math.floor(baseCost * injuryMultiplier)
end
The medical bill creates an economic consequence for dying that balances the server economy. Consider scaling the bill based on how the player was injured, with higher costs for reckless behavior like high-speed crashes versus lower costs for being a crime victim. This encourages careful driving and adds weight to dangerous situations.
Auto-Respawn Fallback System
When no EMS players are on duty, the server must provide an alternative recovery path so downed players are not permanently stuck. Implement a countdown timer that appears after a configurable delay, allowing the player to respawn at the hospital with a flat medical fee deducted automatically. The respawn location should be at the hospital entrance or a designated recovery area. Always check the current EMS count before showing the auto-respawn option, and dismiss it immediately if an EMT claims the call during the countdown. This dual-path system ensures players always have a way to recover while still prioritizing the roleplay experience of EMS interaction when medics are available.
Medical Supplies and Inventory Integration
EMS players need access to medical supplies through a job-restricted stash at the hospital and through a crafting or purchasing system. Stock the hospital stash with bandages, painkillers, splints, IV drips, blood bags, and defibrillator charges. Each treatment action consumes the corresponding item, creating ongoing demand for resupply. Track supply usage in the database to monitor consumption patterns and automatically restock hospital stashes at configurable intervals. Consider making some supplies craftable by players with the pharmacist or doctor role, creating additional roleplay opportunities and economic connections between different jobs on the server.
Logging and Performance Monitoring
Log every EMS interaction for administrative review, including who was treated, what treatments were applied, the responding EMT, and the final medical bill. Store these logs in a dedicated database table and optionally send summaries to a Discord webhook for real-time staff monitoring. These logs help resolve disputes, identify EMS players who are not performing their duties, and track overall system health. On the performance side, keep the death check logic lightweight by only running it on players whose health has dropped to zero, not on every player every frame. Use event-driven architecture where the death state triggers a single server event rather than polling. The dispatch notification system should batch alerts rather than sending individual events to each EMS player, reducing network overhead during mass-casualty scenarios where multiple players go down simultaneously.