Doctor Job¶
hobbs_doctorjob, current version 1.0.0
Overview¶
This is the whole medical side of a server in one resource. Players catch diseases and pick up wounds from the things that happen to them: a gunfight leaves a bullet in the leg, a fall breaks a bone, a swamp gives malaria, a bad meal gives dysentery. Every condition does something you feel, drains health over time and, left alone, can get worse.
Doctors examine patients on a patient record, see the findings on a body silhouette, treat each one step by step with real material (wash, stitch, splint, serum) and three small minigames for the hard steps. There is an x-ray machine for the things you can't see from outside, an examination bed, medical records per practice, prescriptions, a doctor's journal and a pharmacopoeia anyone can read.
When nobody is around, a country doctor NPC in each practice patches people up for a small fee, and an own death screen lets an unconscious player call a doctor or, with nobody on duty, the emergency doctor. Fist fights knock out instead of killing. Everything a player's body carries is stored per character and survives relogs and restarts.
Fourteen conditions ship: bleeding, animal bite, gangrene, broken bone, sprain, snakebite, plant poisoning, rabies, cold, pneumonia, malaria, cholera, typhoid and dysentery. Forty-five items come with them. English and German are included.
How it plays¶
As a patient¶
You get hurt. A revolver hit has a small chance per shot to start a bleed, a knife a much bigger one. You see a message ("You start to bleed. Put on a bandage and see a doctor."), you walk with a limp, sprinting throws you to the ground, and your health drains. Bleeding empties a full health bar in three minutes. A bandage from your pocket stops it for five to ten minutes, a herbal poultice for up to fifteen, yarrow powder a little longer. That buys time, it does not heal. After five bandages the wound is too raw and no bandage holds any more, and after twenty minutes without a doctor there is a ten percent chance the wound turns to gangrene.
The same idea runs through every condition. Something causes it, it has a clock, some items borrow time, and one thing ends it for good:
| Condition | How you catch it | What it does | What you can do yourself | What ends it |
|---|---|---|---|---|
| Bleeding | Gunshot or blade hit, chance per weapon | Limp, falls on sprint, health gone in 3 min | Bandage, leather bandage, herbal poultice, yarrow powder (hold 5 to 15 min, 5 times at most) | Doctor washes, removes the bullet and stitches |
| Animal bite | Predator bite (wolf, coyote, cougar, panther, bear, alligator) | Wound, 5 min drain, fast pulse | Same bandages as bleeding | Doctor washes and stitches. Untreated 30 min: 10 % rabies |
| Gangrene | Bleeding left untreated for 20 min, 10 % chance | 12 min drain, fever | Nothing | Doctor cuts it out with carbolic acid, then stitches |
| Broken bone | A fall with 91+ damage on an arm or leg | Limp, running hurts, 80 min drain, never heals alone | Nothing | X-ray, then the doctor splints it |
| Sprain | A fall with 40 to 90 damage on a limb | Limp, no running or jumping, no health loss | Laudanum, wintergreen liniment or a leech ease it. Heals alone in 5 to 30 min | Rest. The doctor can only give laudanum or liniment |
| Snakebite | A snake | 10 min drain, vomiting, seizures, the poison screen after 5 min | Theriac holds it 10 min, twice at most. Antidote cures | Antidote, yours or the doctor's |
| Plant poisoning | Only given by other scripts through the export | Vomiting, seizures, drains to a quarter of health, gone after 15 min | Emetic brew cures | Emetic brew |
| Cold | About four minutes in the cold or in water. Clothes add warmth | Cough, weak stamina, 100 min drain, gone after an hour | Cough syrup or chest elixir cure, teas and the cough drop sometimes | Any cold remedy. Untreated 25 min: 20 % pneumonia |
| Pneumonia | A cold left untreated | Deep cough, 25 min drain | Pleurisy root tea holds it 20 min, three times | Doctor applies a mustard plaster |
| Malaria | About eight minutes in a contaminated zone | Vomiting, seizures, 30 min drain | Fever mixture cures, quinine 70 %. Healing draught, herbal tea and fever bitters borrow an hour or more | Fever mixture at the doctor |
| Cholera | A contaminated zone | Stomach cramps, 20 min drain | Fever mixture cures. Cholera drops, draught and tea borrow time | Fever mixture at the doctor |
| Typhoid | A contaminated zone | The fastest fever, 15 min drain | Fever bitters hold it 15 min, twice | Fever mixture, only the doctor's |
| Dysentery | Food or drink, one in a thousand uses | Cramps, weak, 50 min drain, never passes alone | Poppy tea cures, blackberry cordial sometimes. Stomach tea and wormwood bitters borrow time | Doctor gives stomach tea, poppy tea or cordial |
| Rabies | An untreated animal bite | Cramps, 20 min drain | Nerve drops hold it 10 min, twice | Pasteur serum from the doctor |
A raised neckerchief freezes the clock in a contaminated zone, so does mosquito oil (malaria) or four thieves vinegar (cholera and typhoid) for half an hour. Laudanum and morphine only work with a prescription in your own name, unless you are a doctor.
/health shows you how you feel, including what nobody can see from outside. Type it any time you are conscious.
When your health runs out you are unconscious. Instead of the usual death screen you lie there with a heartbeat line and two choices. Give up opens after five minutes and puts you at the nearest practice, waking up "a few hours later". Call a doctor is there while a doctor is on duty: a pigeon flies off, every doctor on duty gets a message and a map marker, and your countdown grows by ten minutes. Once a doctor takes the call you see how far away they are. If no doctor is on duty the second choice is the emergency doctor: ten dollars, and an NPC doctor revives you after twenty seconds to two minutes and clears everything. The countdown pauses while someone carries you or you lie on an examination bed. Jailed players get neither call.
While you are conscious, /calldoctor sends the same pigeon. There is a thirty second cooldown between calls.
Giving up does not heal you. Wounds survive death, a bullet is still in your leg when you wake up at the practice.
A fist fight is different: below fifteen percent health you are knocked out instead of killed. You go down for twelve seconds, recover a little health over the next twelve, and can't be knocked out again for five minutes.
As a doctor¶
You need the job (doctor by default) and, if your server has a duty system wired into the bridge, you need to be on duty. Aim at a player within two metres and the Examine prompt appears. The patient record opens: name, pulse, temperature, a body silhouette with the findings, and tabs for notes and prescriptions.
Every finding shows where it is, what state it is in and what material the next step needs. A lodged bullet goes: x-ray, remove the bullet (carbolic acid or sage wash), stitch (suture thread), done. A through-and-through wound skips the x-ray, a graze is one wash. If the patient wears a bandage you take it off first (free), which puts the wound back to the state it was in, and the patient gets three minutes without drain while you work. Click a finding, the material is taken from your pocket, you play the animation and, for bullet, stitch and splint, a minigame with the mouse. Fail it and the material is gone, the wound stays. Esc cancels a minigame. Diseases without a wound (a cold, malaria, dysentery) are cured outright with one of the listed medicines from your pocket, so at the doctor a stomach tea cures dysentery for good where at home it only holds for half an hour.
Some things you can't tell from the outside. A broken bone looks like a sprain, a lodged bullet like a flesh wound: the record says "Unclear finding, X-ray needed". Have the patient stand at the x-ray spot of the practice, open the record and press X-ray, or type /xray. The patient walks to the spot, the lights go blue, and you click the regions on the skeleton. Every shot reveals what sits in that region. The patient sees the same picture on his side. DEL ends it, after a minute it ends by itself.
A bleeding patient you can't treat right now gets Apply bandage from the record. It uses a bandage from your pocket and holds the bleed like a self-applied one. For a sprain you hand over laudanum or liniment, it eases the pain, nothing more.
An unconscious patient: use a revival syringe (always works) or heart drops (sixty percent) from your inventory, a picker shows the unconscious players within three metres. Both items are doctor-only by default. Diseases stay after a revive, so the record is the next step.
/inspect next to a dead body opens a post-mortem: the same record, read only, with the place of the last hit marked.
At an examination bed you can lie a carried patient down (Lay patient down), examine whoever lies there and take them off again. A conscious patient can lie down and get up by himself.
The Notes tab is the medical record. You must stand inside a practice for it, and the note belongs to that practice: every doctor of the house sees it. Incident is required, diagnosis and treatment are prefilled from the record, a follow-up can be ticked. Deleting needs job grade 4. The Prescription tab writes laudanum or morphine for this patient, up to three doses on a sheet. Every dose taken is struck off.
The doctor's journal item opens all records of all practices, searchable by patient, with a reference of every condition and every medicine. From there you make a transcript, a paper copy of one record that can be handed to a sheriff or a relative. The pharmacopoeia is the same reference as a book anyone can own and read.
A practice blip on the map changes colour while a doctor on duty is in that town. The training patient in Saint Denis lets doctors practise the three minigames without material.
The country doctor¶
Each practice has an NPC doctor as long as fewer than two doctors are on duty. Treat me clears every condition and heals for five dollars (he sends you off if nothing is wrong). Revive wakes an unconscious player next to you for eight dollars, after thirty seconds of work. Insured patients (see the bridge) pay half. Ten percent of his income can go to the doctors' company account if your money script is wired in.
Requirements¶
| Resource | Note |
|---|---|
vorp_core |
Characters, jobs, money, revive |
vorp_inventory |
Items, usable items, prescription and transcript metadata |
vorp_lib |
The player picker for revive, inspect and the country doctor |
nss_libs |
Prompts, blips, client and server event helpers |
oxmysql |
Disease state, practices and medical records |
jo_libs |
Reads whether the neckerchief is up. See the bridge if you want to drop it |
vorp_progressbar |
Optional. Shows the progress of item use and treatment steps, without it the script just waits |
Create the job doctor in VORP (or rename the entries in config/doctor.lua). Note deletion looks at job grade 4.
Installation¶
- Unpack the resource into
resourcesand keep the folder namehobbs_doctorjob. - Run
sql/install.sqlonce. It creates three tables:doctor_diseases(one row of disease state per character),doctor_hospitals(one row per practice, filled on start) anddoctor_notes(the medical records). It also inserts the forty-five items withINSERT IGNORE, so items you already have keep their labels. - Copy the item images from
install/tovorp_inventory/html/img/Items/and restartvorp_inventory. - Create the job
doctorin VORP and give your doctors a grade. Grade 4 and up may delete records. -
Add the resource to
server.cfgafter its dependencies: -
Put your Discord webhook URLs into
server_config.luaif you want logs. - Start the server. On first start check the console for warnings about models that don't load, then walk to a practice: the blip is there, the country doctor stands inside, and
/healthopens your own report.
The script ships with six practices, seven x-ray machines, one examination bed, twelve respawn points and three contaminated zones. They are examples from the game's map. Keep what fits your server and replace the rest.
Warning
The resource brings its own death screen. If you run another death or respawn script, either turn Config.Features.emergency off or remove the other one. Two scripts fighting over a dead player ends badly.
Configuration¶
Everything lives in config/, one file per topic. All of them are open files. Texts are in locales/en.lua and locales/de.lua, the handbook texts in locales/en_book.lua and locales/de_book.lua.
core.lua¶
Language, feature switches, commands and the things that don't fit anywhere else.
Config.Locale = 'en' -- 'en' or 'de'
Config.Debug = false
Config.Features = {
xray = true, -- x-ray machines; off: fractures and bullets need no x-ray
countryDoctor = true, -- NPC doctor in the practices while few doctors are on duty
emergency = true, -- own death screen with doctor call and NPC emergency doctor
knockout = true, -- fist fights knock out instead of killing
crutch = false, -- check the hand position in config/crutch.lua first
pharmacopoeia = true, -- the book of remedies, readable by anyone who has it
doctorBlips = true, -- practice blips change colour while a doctor is on duty
training = true, -- training patient for the wound minigames
minigames = true, -- off: a progress bar instead of the wound minigames
foodDiseases = true, -- bad food and water can cause dysentery
}
Config.AdminGroups = { 'admin' }
Config.Commands = {
selfCheck = 'health',
inspect = 'inspect',
xray = 'xray',
emergencyCall = 'calldoctor',
acceptCall = 'acceptcall',
declineCall = 'declinecall',
giveDisease = 'disease_give',
healDisease = 'disease_heal',
}
Config.WorldBaseYear = 1896 -- every shown date is in this year
Config.Insurance = { share = 0.5 }
Config.ItemImages = 'nui://vorp_inventory/html/img/Items/'
Config.ProgressBar = { color = '#c41e1e', width = 30 }
A feature that is off starts nothing and registers no item, so you lose nothing by switching parts off that you don't want. crutch is off because the crutch prop sits in the hand by offset and rotation, and I want you to look at it in game before players see it. minigames = false gives you a plain progress bar for every treatment step, which is the right call for a server with many players who hate minigames. foodDiseases listens to every item use on the server, so if you don't want dysentery at all, turn it off here instead of setting the chance to zero.
Insurance.share is the share an insured patient pays at the country doctor and the emergency doctor. Who counts as insured is decided in bridge/server.lua. With the default bridge nobody is.
hospitals.lua¶
The practices: blip, catchment area for notes and the country doctor.
Config.Hospitals = {
['valentine'] = {
label = 'Valentine Doctor',
showBlip = true,
blipSprite = -1739686743,
coords = vector3(-285.68, 810.36, 119.39),
radius = 30.0, -- catchment area for notes and treatment
countryDoctor = {
enabled = true,
jobs = { 'doctor' }, -- doctors of these jobs close him from two on duty
healCost = 5.0,
reviveCost = 8.0,
reviveTime = 30, -- seconds
coords = vector3(-288.47, 813.23, 118.39),
heading = 170.25,
range = 2.0,
},
},
-- saint_denis, rhodes, blackwater, strawberry, armadillo
}
Config.CountryDoctor = {
recountMs = 15000,
selectRange = 5.0,
companyShare = { rate = 0.10, job = 'doctor' }, -- job = nil turns it off
models = { 'cs_drmalcolmmacintosh', 'u_m_m_valdoctor_01', 'cs_creoledoctor',
'u_m_m_rhddoctor_01', 'cs_sddoctor_01' },
scenarios = { 'WORLD_HUMAN_SMOKE_CIGAR', 'MP_LOBBY_STANDING_A', 'WORLD_HUMAN_WRITE_NOTEBOOK' },
}
Config.DoctorBlips = {
pollMs = 5000,
townRadius = 250.0, -- for practices outside any named district
activeColor = 'BLIP_MODIFIER_MP_COLOR_8',
inactiveColor = 'BLIP_MODIFIER_MP_COLOR_32',
recheckMs = 5000,
}
The key ('valentine') is stored with every medical record, so don't rename a practice once it has notes. Add a practice by copying a block with a new key. radius matters more than it looks: a doctor must stand inside it to write a note, and the patient must be in the same practice. Thirty metres covers a doctor's house with its porch; if your practice is a whole hospital building, raise it. Set countryDoctor = nil for a practice that should have no NPC, for example a prison infirmary. The model is picked from the practice key, not rolled, so every player sees the same face. The company share goes through Bridge.payCompany and is silently skipped when that returns false.
doctor.lua¶
The job, targeting, revive rules, the minigames and the training patient.
Config.Doctor = {
jobs = { 'doctor' },
requireOnDuty = true, -- asks Bridge.isOnDuty, which says yes by default
targeting = { players = true, range = 2.0, scanIdleMs = 1000, scanAimingMs = 100 },
inspect = { jobs = { 'doctor' }, range = 5.0 },
revive = {
range = 3.0,
noHeadshot = false, -- country doctor refuses headshot victims
clearDiseases = false, -- false: wounds survive death and need treatment
},
minigames = {
['cut'] = { game = 'suture', settings = { stitches = 5, lives = 2, time = 40 } },
['projectile'] = { game = 'bullet', settings = { lives = 2, time = 30, width = 21, shake = 2.2 } },
['broken'] = { game = 'splint', settings = { lives = 2, time = 45, holdMs = 5000 } },
},
}
Config.Training = {
npcModel = 'cs_sddoctor_01',
coords = vector3(2717.21, -1239.27, 33.03),
heading = 245.34,
range = 1.5,
buildRange = 25.0,
jobsOnly = true,
}
jobs can hold several job names if your server has a doctor and a nurse job, both get the same rights. clearDiseases = false is the heart of the script: an unconscious player who is revived still carries every wound, and someone has to treat them. Set it to true if you want revive to be a full reset like most servers run it. The minigame settings are difficulty: width is half the channel the forceps may move in, shake the hand tremor, time the seconds you get. If your doctors complain that the bullet is impossible, raise width to 28 and lower shake to 1.5. Each file lists every setting with its meaning.
beds.lua¶
Examination beds and how a patient lies on them.
Config.Beds = {
places = {
{ model = 'p_medbed01x', coords = vector3(-286.21, 816.14, 118.39), heading = 10.0,
label = 'Valentine' },
},
extraModels = {}, -- further bed models without a known position
attach = {
default = { x = -0.714, y = -0.013, z = 0.968, pitch = 0.0, roll = 0.0, yaw = 180.0 },
female = { x = -0.614, y = -0.113, z = 0.908, pitch = 0.0, roll = 0.0, yaw = 180.0 },
},
range = { use = 1.0, find = 1.0, place = 1.3, getUp = 2.0, state = 3.5 },
holdMs = 900000, -- longest an unconscious patient is held on the bed
}
A bed is a map prop, the script does not spawn one. places tells the script which prop at which spot is a bed; the nearest one is used. Only Valentine is set up, add a line per bed you want to use in the other practices. extraModels is for bed models you want to count anywhere without a known position. The attach numbers are the head position in bed space; they fit p_medbed01x and you only touch them for another bed model. holdMs must outlast your longest treatment, fifteen minutes is generous.
diseases.lua¶
Every condition: how it is caught, how fast it drains, what it does to the body and what the doctor needs.
Config.Diseases = {
['bleeding'] = {
drain = { minutes = 3 }, -- time to empty a full health bar, 0 = no drain
effectMs = 6000,
tickMs = 250,
selfHealMs = -1, -- -1 never, ms, or { minMs, maxMs }
maxEases = 5, -- bandages before the wound is too raw
cause = {
kind = 'damage',
damageTypeFirearm = 'projectile',
damageTypeMelee = 'cut',
firearmSplit = { -- must add up to 1.0
{ type = 'grazed', chance = 0.50 },
{ type = 'perforated', chance = 0.30 },
{ type = 'projectile', chance = 0.20 },
},
firearms = { ['weapon_revolver_cattleman'] = 0.08, ['weapon_rifle_elephant'] = 0.15, --[[ ... ]] },
melee = { ['weapon_melee_knife'] = 0.4, --[[ ... ]] },
},
complication = {
disease = 'infection', chance = 0.10, afterSeconds = 1200, when = 'untreated',
chanceByItem = { herbal_poultice = 0.0, bandage_native = 0.5 },
},
effects = { loco = 'injured_general', wound = true, ragdollOnSprint = true,
healthRecharge = 0, camShake = 0.1 },
},
-- brokenBone, sprain, animalBite, snakeBite, plantPoison, cold, malaria,
-- cholera, typhus, dysentery, infection, rabies, pneumonia
}
Config.DiseaseOrder = { 'bleeding', 'animalBite', 'infection', 'brokenBone', 'sprain', 'snakeBite',
'plantPoison', 'rabies', 'cold', 'pneumonia', 'malaria', 'cholera', 'typhus', 'dysentery' }
Config.Relief = { ['sprain'] = { items = { 'laudanum', 'wintergreen_liniment' }, durationMs = 900000 } }
Config.Consume = { items = { ['consumable_stew'] = true, ['water'] = true } } -- examples
Config.ClothesWarmth = { [0xE06D30CE] = 6.0, --[[ coat ]] [0x2026C46D] = 2.0, --[[ shirt ]] }
drain.minutes is the knob you will touch most. It is the time from full health to the ground, so it scales with max health. Three minutes for bleeding is harsh on purpose: a bandage is cheap and every player should carry one. If your server is slower, twenty minutes gives people time to ride to town. selfHealMs is the other one: a cold passes after an hour by itself, bleeding never does.
The cause block differs by kind. damage rolls per weapon hit, animal per predator bite, bite for snakes, cold from the felt temperature (outside temperature plus the warmth of the clothes worn), zone from standing inside a radius (one point per second, outbreak at maxLevel), consume from item use, complication only from another disease. The zones are three example spots on the map, move them to where your swamps and ghost towns are. blockedByBandana lets a raised neckerchief freeze the zone clock.
complication is what happens when a condition is left alone: after afterSeconds the server rolls once with chance. when = 'untreated' rolls even while a bandage holds, and chanceByItem lets a better bandage lower or pause the roll. maxEases caps how often time-borrowing items may be used before only a doctor helps.
A new disease also needs an entry in Config.DiseaseOrder (a disease missing there is invisible everywhere) and its texts in every locale: the name under diseases, the messages under diseaseTexts, and a handbook entry in the book file.
wounds.lua¶
The wound states and the doctor's step for each one.
Config.DamageTypes = {
['projectile'] = { -- lodged bullet: x-ray, remove, stitch
severity = 4,
next = 'cut',
needsXray = true,
dict = 'amb_work@world_human_repair@med@oilcan@male_a@idle_b',
anim = 'idle_d',
duration = 5000,
doctorItems = { 'carbolic_acid', 'sage_wash' },
},
['grazed'] = { severity = 1, next = nil, duration = 10000, doctorItems = { 'carbolic_acid', 'sage_wash' } },
['perforated'] = { severity = 3, next = 'cut', duration = 15000, doctorItems = { 'carbolic_acid', 'sage_wash' } },
['cut'] = { severity = 2, next = nil, duration = 20000, doctorItems = { 'suture_thread' } },
['bitten'] = { next = 'cut', duration = 15000, doctorItems = { 'carbolic_acid', 'sage_wash' } },
['broken'] = { next = nil, duration = 20000, doctorItems = { 'wooden_splint' } },
['sprained'] = { next = nil, duration = 6000 }, -- material from Config.Relief
['necrotic'] = { next = 'cut', duration = 25000, doctorItems = { 'carbolic_acid' } },
['rabid'] = { next = nil, duration = 12000, doctorItems = { 'rabies_serum' } },
['illness'] = { next = nil, duration = 15000, doctorItems = { 'mustard_plaster' } },
['bandaged'] = { next = 'cut', duration = 10000, doctorItems = {} }, -- removing a bandage costs nothing
['default'] = { next = nil, duration = 20000 }, -- the disease's own doctorItems apply
}
A wound is a chain: next is the state after this step, nil means treated. doctorItems is a list, one of them is used up, so carbolic acid and sage wash are two ways to wash a wound. severity stops a second hit from downgrading a wound: a graze on top of a lodged bullet stays a lodged bullet. duration is the animation time and the progress bar when no minigame runs. Want gunshots to be cheaper? Give grazed an empty doctorItems = {} and a wash costs nothing. The progress texts ("Removing the bullet...") sit under treatmentTexts in the locales.
medicines.lua¶
Every usable item: what it cures, for how long, what it heals, how it is used.
Config.Medicines = {
['bandage'] = {
cureChance = { bleeding = 1.0, animalBite = 1.0 },
cureMs = { minMs = 300000, maxMs = 600000 }, -- borrows time, rolled per use
useMs = 10000,
health = 40,
healthCore = 20,
scenario = 'WORLD_HUMAN_CROUCH_INSPECT',
damageType = 'bandaged', -- wound state it leaves behind
},
['antibiotic'] = {
cureChance = { cholera = 1.0, malaria = 1.0, typhus = 1.0 },
cureMs = -1, -- cures for good
useMs = 5000,
health = 100,
healthCore = 50,
anim = { dict = 'amb_rest_drunk@world_human_drinking@male_a@idle_a', anim = 'idle_a' },
},
['fever_bitters'] = {
cureChance = { malaria = 1.0, typhus = 1.0 },
cureMs = 5400000,
cureMsFor = { typhus = 900000 }, -- per disease override
useMs = 4000, health = 20, healthCore = 10,
},
['morphine'] = { cureChance = {}, cureMs = -1, useMs = 2400, health = 200, healthCore = 100 },
-- ...
}
Config.Revives = {
['syringe'] = { reviveChance = 1.0, useMs = 10000, health = 100, healthCore = 50, jobs = { 'doctor' } },
['heart_drops'] = { reviveChance = 0.6, useMs = 10000, health = 60, healthCore = 30, jobs = { 'doctor' } },
}
Config.Protections = {
['mosquito_oil'] = { protects = { 'malaria' }, durationMs = 1800000, useMs = 5000 },
['four_thieves_vinegar'] = { protects = { 'cholera', 'typhus' }, durationMs = 1800000, useMs = 5000 },
}
Config.Prescription = {
item = 'prescription',
medicines = { 'laudanum', 'morphine' },
maxDoses = 3,
}
cureMs is the difference between a remedy and a cure. -1 ends the disease, a number of milliseconds pauses it and the disease comes back with the item's expired message. cureChance below 1.0 means the item sometimes does nothing, the player still loses it. An item with an empty cureChance is a tonic: it only restores health. health and healthCore are what the player gets after useMs, on top of any cure.
The doctor reads this list too: every item with damageType = 'bandaged' is a bandage he can apply from the record. Revives with jobs = {} lets anyone use the syringe. Protections are client side and gone after a relog, a second use restarts the timer.
To add a medicine, add it here, insert the item, drop an image into the inventory and add its messages under itemTexts and its name under items in both locales. The pharmacopoeia picks it up by itself.
pharmacopoeia.lua¶
The book of remedies, a plate per item.
Config.Pharmacopoeia = {
item = 'pharmacopoeia',
scenario = 'WORLD_HUMAN_WRITE_NOTEBOOK', -- reading pose, false = none
chapters = {
{ id = 'wounds', items = { 'bandage', 'bandage_native', 'yarrow_powder', 'herbal_poultice', 'leech',
'laudanum', 'wintergreen_liniment', 'crutch' } },
{ id = 'cough', items = { 'consumable_medicine', 'cough_syrup', 'cold_tea', 'native_tea', 'thyme_balm',
'consumable_cough_drop', 'pleurisy_tea' } },
{ id = 'fever', items = { 'antibiotic', 'quinine_powder', 'fever_bitters', 'cholera_drops', 'herbal_tea',
'native_medicine', 'mosquito_oil', 'four_thieves_vinegar' } },
{ id = 'stomach', items = { 'poppy_tea', 'blackberry_cordial', 'stomach_tea', 'wormwood_bitters' } },
{ id = 'poison', items = { 'antidote', 'theriac', 'emetic_brew', 'nerve_drops' } },
{ id = 'strength', items = { 'syringe', 'heart_drops', 'morphine', 'ginseng_tonic' } },
{ id = 'tools', items = { 'carbolic_acid', 'sage_wash', 'suture_thread', 'wooden_splint',
'mustard_plaster', 'rabies_serum' } },
},
stamps = { crutch = 'doctor' }, -- 'prescription', 'doctor' or false
}
The chapters only decide the order. Effects, cures and stamps ("Prescription only", "For the doctor") are read from the other configs, so you rarely touch this file. An item in no chapter lands in a last chapter called "more", an item the script doesn't use drops out. Chapter titles are under book.chapters in the book locale.
emergency.lua¶
The death screen, the doctor call and the emergency doctor.
Config.Emergency = {
respawnSeconds = 300, -- until "Give up" opens
afterCallSeconds = 600, -- added once a doctor was called
callCooldownMs = 30000,
callExpireSec = 1800, -- an unanswered call lapses
npcDoctorCost = 10,
npcDoctorDelayMs = { 20000, 120000 },
deathFilter = false, -- the game's grey filter
hideUi = true,
ragdollOnRevive = true,
controllableCamera = true, -- free look while lying
keyRespawn = 0xCEFD9220, keyRespawnLabel = 'E',
keyCallDoctor = 0xE30CD707, keyCallLabel = 'R',
holdMs = 1500,
blockControls = true,
allowedControls = { 0x1F6D95E5, 0x9720FCEE }, -- F4, T (chat)
callMarker = { sprite = -1739686743, color = 'BLIP_MODIFIER_MP_COLOR_8' },
respawnPoints = {
{ name = 'Valentine', pos = vector4( -289.49, 820.34, 118.80, 15.83) },
{ name = 'Saint Denis', pos = vector4( 2724.04, -1220.35, 49.39, 267.73) },
-- Blackwater, Rhodes, Strawberry, Annesburg, Armadillo, Tumbleweed,
-- Guarma, Wapiti, Colter, Van Horn
},
districts = { towns = { --[[ ... ]] }, regions = { --[[ ... ]] }, states = { --[[ ... ]] } },
}
respawnSeconds is how long a player has to wait before giving up, and afterCallSeconds what a doctor call adds. Five plus ten minutes means a doctor across the map still arrives in time. Make respawnSeconds short on a server with few doctors, nobody likes lying in the dirt. allowedControls are the keys that still work while dead, chat and the pause menu by default; add your radio key here. respawnPoints are where a player wakes up after giving up, the nearest one is used, so cover your map. districts turns a position into a place name for the call text ("Someone lies unconscious: Rhodes."), you only touch it for a new map.
xray.lua¶
The x-ray machine is a map prop, the resource only adds the session.
Config.Xray = {
requireRange = 1000.0, -- x-ray only required within this distance from a machine
jobs = nil, -- nil = Config.Doctor.inspect.jobs
durationMs = 60000,
cancelKey = 0x4AF4D473, -- DEL
machineRange = 5.0, -- doctor to machine
patientRange = 5.0, -- patient to his spot
machine = {
places = {
{ coords = vector3(-285.47, 814.44, 119.39), heading = 198.29, label = 'Valentine',
patient = vector4(-284.81, 811.85, 118.39, 23.34) },
-- Saint Denis, Blackwater, Strawberry, Armadillo, Sisika, Van Horn
},
},
look = { patientAlpha = 90, skeleton = { enabled = true, mode = 'model', model = 'a_m_m_unicorpse_01' } },
sound = { enabled = true, radius = 15.0, volume = 0.5 },
light = { enabled = true, range = 9.0, intensity = 90.0 },
sparks = { enabled = true, radius = 20.0, every = 1600 },
}
coords is the machine, patient where the patient stands and faces. requireRange is a kindness: a bullet only needs an x-ray when a machine is within a kilometre of the patient, so a camp doctor in the mountains can still pull a bullet. Set it very high to require the x-ray everywhere, or turn the feature off and nothing ever needs one. The skeleton can be a ped model (mode = 'model') or drawn lines (mode = 'lines'). Sound, light and sparks are for bystanders and cost a little each frame while a session runs, turn them off on a weak server.
knockout.lua¶
Config.Knockout = {
onlyMelee = true,
healthPercent = 15.0, -- of the real max health
ragdollMs = 12000,
recoverySeconds = 12,
regenPercentPerSecond = 2.5, -- up to healthPercent
cooldownMs = 300000,
screenFx = 'DeathFailMP01',
soundName = 'Heartbeat', soundSet = 'RDRO_Sniper_Tension_Sounds', soundEvery = 2000,
}
Below healthPercent in a fist fight the player goes down for ragdollMs, is invincible meanwhile, and gets back a little health. onlyMelee = false would knock out on any damage, which I don't recommend. Never fires in a PvP match (see Bridge.inPvpMatch).
crutch.lua¶
Off by default. While held: walk only, no running or jumping, so a player with a hurt leg stops falling over. It is put away by itself when mounting, in a coach, swimming, with a weapon drawn, on the bed and on death.
Config.Crutch = {
item = 'crutch',
uses = 10, -- breaks after this many pick-ups
model = 'p_crutchjoe01x',
bone = 'PH_L_Hand',
offset = vector3(0.14, -0.04, 0.01),
rotation = vector3(-20.0, -102.0, 2.0),
maxMoveBlend = 1.0, -- 1.0 walk, 2.0 run, 3.0 sprint
jumpKey = 0xD9D0E1C0, -- SPACE, blocked while held
}
Check offset and rotation in game before you switch the feature on, the prop should sit under the arm. The item carries its remaining uses in its metadata and the description tells the player how many walks are left.
records.lua¶
Config.Records = {
notes = {
maxLength = 500, -- per field, cut not rejected
followupMax = 200,
deleteAny = true, -- false: only own notes
deleteGrade = 4, -- minimum job grade to delete
maxPerPatient = 20, -- the oldest drops out
},
journal = { item = 'doctor_journal', pageSize = 50 },
transcript = { item = 'medical_record', seconds = 15, anim = { scenario = 'WORLD_HUMAN_WRITE_NOTEBOOK' } },
}
The journal needs the job, not duty, so a doctor at home can still read up on a patient. deleteGrade is checked on the server. If your doctors' job has only three grades, lower it or nobody can ever delete a note.
effects.lua¶
How wounds and diseases show on the body: the walk style per body part, the injury pose others can see, the seizure clips, the grace time after a bandage comes off.
Config.Effects = {
staminaRechargeNormal = 0.20,
seizureMs = 5000,
ragdollMs = { 4000, 10000 }, -- on the ground after sprinting with a bad leg
woundGraceMs = 180000, -- no drain after the doctor removes a bandage, 0 = off
locomotionRank = { ['right-leg'] = 4, ['left-leg'] = 4, ['torso'] = 3, ['right-arm'] = 2, ['head'] = 1 },
injuryPose = { enabled = true, flag = 31, checkMs = 500, weaponGraceMs = 2500, wheelGuard = true },
partAnims = { ['left-leg'] = { dict = 'mech_loco_m@generic@injured@unarmed@left_leg@idle', clip = 'idle' } },
}
Walk styles are local only, the injuryPose is what other players see. woundGraceMs gives the patient three minutes without drain after the doctor takes a bandage off, long enough for the stitching. You rarely change this file.
body.lua¶
Bone ids per body part, which silhouette region each part lights, healthy vitals and the pulse ranking for the record. Leave it alone unless you add a new part to the silhouette.
Config.Body = {
parts = { ['head'] = { 21030 }, ['right-leg'] = { 11440, 11456, 43312, 53081, 55120, 65478 }, --[[ ... ]] },
healthyVitals = { pulse = 'normal', temperature = { 36.0, 37.5 } },
pulseRank = { normal = 0, fast = 1, weak = 2, extreme = 3 },
}
bridge/client.lua and bridge/server.lua¶
The bridge is where the script meets your other resources. Both files are open, every function has a comment with what it gets and what it must return. Change the body, keep the name.
| Function | Default | Change it when |
|---|---|---|
Bridge.notify(kind, text, itemName) |
vorp_core left notification |
You use another notification script |
Bridge.inPvpMatch() |
false |
You run arenas or boxing rings. There fights leave no wounds and never knock out |
Bridge.isJailed() (client and server) |
Reads the state bag isInJail |
Your jail script uses another flag |
Bridge.respawnJailed() |
Nothing | A jailed player who gave up should wake in his cell |
Bridge.isMaskUp() |
Reads the neckerchief through jo_libs |
You want to drop jo_libs, then return false here and remove it from fxmanifest.lua |
Bridge.clothingWarmth() |
Config.ClothesWarmth |
Your HUD has its own warmth table, return it so both agree |
Bridge.isOnDuty(source) |
true |
You have a duty system. Call exports.hobbs_doctorjob:refreshDuty() when duty changes |
Bridge.isInsured(source, charId) |
false |
You sell health insurance |
Bridge.payCompany(job, amount, reason, payer) |
false |
Your society script has company accounts |
Bridge.isFood(itemName, metadata) |
Hunger or thirst in the metadata, or listed in Config.Consume.items |
Your food items are recognised differently |
Bridge.sendDoctorCall(call) |
false, the built-in notification and marker are used |
You have a dispatch script. Report back with the callAccepted, callDeclined and callResolved exports |
Bridge.closeDoctorCall(patient) |
Nothing | Your dispatch should close the call when the patient is helped |
A duty example: if your duty script sets the state bag onDuty, the server bridge becomes
server_config.lua¶
Discord logs, never sent to players.
ServerConfig = {
webhooks = {
diseases = '', -- diseases coming and going, the loudest log
treatment = '', -- treatments by doctors and items
npcdoctor = '', -- country doctor fees and treatments
emergency = '', -- emergency doctor and lapsed calls
notes = '', -- medical records and transcripts
},
webhookName = 'Doctor',
webhookAvatar = '',
}
Leave a URL empty to keep that channel quiet. diseases fires for every outbreak and every cure on the server, so give it its own channel.
Items¶
All items are inserted by sql/install.sql, images are in install/. Chances and durations are the defaults from config/medicines.lua.
Bandages and wound care¶
| Item | What it does |
|---|---|
bandage |
Holds bleeding or a bite 5 to 10 min, 40 health |
bandage_native |
Leather bandage, same hold, 30 health. Halves the gangrene roll |
herbal_poultice |
Holds 5 to 15 min, 80 health. Pauses the gangrene roll while it holds |
yarrow_powder |
Holds 8 to 12 min, 30 health |
Fever¶
| Item | What it does |
|---|---|
antibiotic |
Fever mixture. Cures cholera, malaria and typhoid, 100 health |
native_medicine |
Healing draught. Holds cholera and malaria an hour |
herbal_tea |
Holds cholera and malaria an hour, works half the time |
fever_bitters |
Holds malaria 90 min, typhoid 15 min |
quinine_powder |
Cures malaria seven times out of ten |
cholera_drops |
Holds cholera 90 min, dysentery 45 min |
Stomach¶
| Item | What it does |
|---|---|
stomach_tea |
Holds dysentery 30 min. At the doctor it cures |
poppy_tea |
Cures dysentery |
blackberry_cordial |
Cures dysentery six times out of ten |
wormwood_bitters |
Holds dysentery 20 min |
Poison and bites¶
| Item | What it does |
|---|---|
antidote |
Cures a snakebite |
theriac |
Holds a snakebite 10 min |
emetic_brew |
Cures plant poisoning, the only thing that does |
nerve_drops |
Holds rabies 10 min |
Cough and cold¶
| Item | What it does |
|---|---|
cold_tea, native_tea |
Cure a cold half the time |
cough_syrup, consumable_medicine |
Cure a cold |
consumable_cough_drop |
Cures a cold four times out of ten |
thyme_balm |
Cures a cold six times out of ten |
pleurisy_tea |
Cures a cold, holds pneumonia 20 min |
Pain and strength¶
| Item | What it does |
|---|---|
laudanum |
Eases a sprain 15 min, 50 core. Prescription only |
wintergreen_liniment |
Eases a sprain 10 min |
leech |
Eases a sprain 15 min |
morphine |
200 health, cures nothing. Prescription only |
ginseng_tonic |
120 health, cures nothing |
Revival and protection¶
| Item | What it does |
|---|---|
syringe |
Revival syringe, always works. Doctors only |
heart_drops |
Revives six times out of ten. Doctors only |
mosquito_oil |
Freezes the malaria clock for 30 min |
four_thieves_vinegar |
Freezes the cholera and typhoid clock for 30 min |
Doctor's material¶
Not usable from the inventory (usable = 0), used up by a treatment step from the record.
| Item | Used for |
|---|---|
carbolic_acid |
Washing a gunshot or bite, cutting out gangrene |
sage_wash |
Washing a gunshot or bite |
suture_thread |
Stitching an open wound |
wooden_splint |
Splinting a fracture |
rabies_serum |
Pasteur serum against rabies |
mustard_plaster |
Pneumonia |
Papers and tools¶
| Item | What it does |
|---|---|
prescription |
Written by a doctor for one patient, carries medicine, doses and names in its metadata |
doctor_journal |
Opens all medical records, the reference and the medicine list. Doctors only |
medical_record |
A transcript, a frozen copy of one record |
pharmacopoeia |
The book of remedies, for anyone |
crutch |
Only registered while Config.Features.crutch is on |
Commands and keys¶
| Command | Who | What |
|---|---|---|
/health |
Everyone | Your own condition, including what nobody can see |
/calldoctor |
Everyone, conscious | Sends a pigeon to the doctors on duty |
/inspect |
Doctor job, on duty | Post-mortem of a dead body within 5 m |
/xray |
Doctor job, on duty | X-rays whoever stands on the spot of the machine you stand at |
/acceptcall |
Doctor on duty | Takes the newest open emergency call |
/declinecall |
Doctor on duty | Drops the call you took, or turns the newest one down |
/disease_give <disease> [id] |
Admin group or console | Gives a disease, for example /disease_give malaria 3 |
/disease_heal <disease> [id] |
Admin group or console | Takes it away again |
Disease names for the admin commands are the keys of Config.Diseases: bleeding, animalBite, infection, brokenBone, sprain, snakeBite, plantPoison, rabies, cold, pneumonia, malaria, cholera, typhus, dysentery. All command names change in Config.Commands.
Prompts and keys:
| Where | Key | Action |
|---|---|---|
| Aiming at a player within 2 m, as doctor | L | Examine, opens the patient record |
| Examination bed | L | Examine whoever lies on it |
| Examination bed | Key shown in the prompt | Lie down, Lay patient down (while carrying one), Take patient off |
| Lying on the bed | Key shown in the prompt | Get up |
| Country doctor | Key shown in the prompt | Treat me, with the price |
| Country doctor | G | Revive, with the price |
| Training patient | L, G and the third shown key | Practise stitching, splinting, bullet removal |
| Death screen | Hold E | Give up, once the countdown is over |
| Death screen | Hold R | Call a doctor, or the emergency doctor when nobody is on duty |
| X-ray | DEL | Ends the session, for doctor and patient |
| Wound minigames | Mouse and wheel, Esc | Steady the hand, cancel |
| Holding a crutch | Space | Blocked |
The record, journal, pharmacopoeia and transcript are pages with the mouse. The record shows a hint line with every button and the keys it listens to. The prompt keys are RedM control hashes in shared/init.lua (DOC.KEY), the death screen keys in config/emergency.lua, DEL in config/xray.lua.
Exports and events¶
Server exports¶
local doc = exports.hobbs_doctorjob
doc:getDiseases(source) -- list of { id, label, damageType, bone, eased } or nil
doc:hasDisease(source, 'malaria')
doc:isInjured(source) -- true while anything is active
doc:giveDisease(source, 'plantPoison', 'ate nightshade') -- false if unknown or already active
doc:healDisease(source, 'plantPoison')
doc:clearDiseases(source, 'arena reset')
doc:listDiseases() -- { id = label, ... }
doc:isDoctor(source) -- job and duty
doc:getDoctorsOnDuty() -- list of server ids
doc:isUnconscious(source)
doc:revive(source) -- false if not down
doc:refreshDuty() -- call when your duty script changes duty
doc:callAccepted(patient, doctor) -- for your own dispatch, see Bridge.sendDoctorCall
doc:callDeclined(patient, doctor)
doc:callResolved(patient)
A herb script that poisons the player who picks the wrong plant:
Client exports¶
exports.hobbs_doctorjob:getDiseases() -- same shape as the server list, for the own player
exports.hobbs_doctorjob:hasDisease('cold')
exports.hobbs_doctorjob:isInjured()
exports.hobbs_doctorjob:isUnconscious()
exports.hobbs_doctorjob:isOnExamBed()
exports.hobbs_doctorjob:openPharmacopoeia() -- false while the feature is off
exports.hobbs_doctorjob:getInjuryLocomotion()
Events¶
Fired on the server for other server resources:
| Event | Arguments |
|---|---|
hobbs_doctorjob:diseaseStarted |
source, diseaseId |
hobbs_doctorjob:diseaseCured |
source, diseaseId, how (doctor, self, admin, script, country_doctor, emergency_doctor, revive) |
hobbs_doctorjob:treated |
doctor, patient, diseaseId, done (done is true when the chain is finished) |
hobbs_doctorjob:revived |
source, by (a doctor's server id, country_doctor, emergency_doctor or script) |
On the client the own player gets hobbs_doctorjob:diseaseStarted and hobbs_doctorjob:diseaseCured with the disease id. A HUD that wants to show a wound icon:
The player state bags doctorInjury, doctorOnBed and doctorCrutch and the global state bags doctorsOnDuty and doctorTowns are set by the script and may be read, not written.
FAQ¶
The Examine prompt never shows.
Three checks. The character's job must be one of Config.Doctor.jobs, the name as VORP stores it. If you wired a duty system into Bridge.isOnDuty, the doctor must be on duty. And you have to aim at the player (right mouse button) within two metres; the prompt is tied to the aimed entity. It also hides while a record is open or while you are down yourself.
The country doctor is not in the practice.
He only stands there while fewer than two doctors of his jobs are on duty, and the count includes every job in that list. With Bridge.isOnDuty left at the default, every online doctor counts as on duty. If the model does not load there is a warning in the client console; switch Config.CountryDoctor.models to models your server has.
"There is no x-ray machine here."
The doctor must stand within machineRange (5 m) of a coords entry in config/xray.lua, and the patient within patientRange of that entry's patient point. Add a line per practice with a machine. Without one, raise requireRange only if you want the x-ray required everywhere; otherwise a far-away practice simply treats without it.
Players bleed out before they reach a doctor.
That is drain.minutes in config/diseases.lua, three for bleeding. Raise it to ten or twenty, or hand out bandages. Every bandage buys five to ten minutes and a doctor can apply one from the record too.
A treatment step says "You are missing the material" but the doctor has the item.
The item must sit in the doctor's own inventory, not a saddlebag or chest, and the name must match doctorItems exactly. Doctor's material is inserted with usable = 0; if you created the items yourself, check the name.
A player can't use laudanum.
Laudanum and morphine are in Config.Prescription.medicines and need a prescription item in that player's name. A doctor writes one on the Prescription tab of the record, three doses by default. Doctors themselves need none. Remove the medicine from the list to free it.
What happens on a restart?
Disease state is written to doctor_diseases on every change and on resource stop, and sent back to the player on start. A running x-ray ends, a patient on a bed is released, a held crutch is put away, the death screen closes. Nothing is lost.
Where does the money go?
The country doctor and the emergency doctor take cash from the paying player. Ten percent of the country doctor's fees are offered to Bridge.payCompany for the doctor job; with the default bridge that does nothing and the money simply disappears. A player doctor's treatment costs nothing in the script, what he charges is roleplay.
How do I add a practice, a bed or a contaminated zone?
A practice is a new key in Config.Hospitals; it is registered in doctor_hospitals on the next start. A bed is a line in Config.Beds.places with the prop model and its position. A zone is a coords and radius entry under the disease's cause.zones in config/diseases.lua; one point per second inside, outbreak at maxLevel.
Is it heavy?
The client scans for a target once a second (ten times a second while aiming), for beds every 750 ms, for the cold every 250 ms and for zones every second. The server recounts doctors every 15 s and rolls complications every 30 s. The x-ray light and sparks are drawn every frame while a session runs; Config.Xray.light and sparks can be turned off. Keep Config.Debug off on a live server.
Can I add a language?
Copy locales/en.lua and locales/en_book.lua, rename the table (Locales.fr = { ... }), add both files to fxmanifest.lua under shared_scripts and set Config.Locale = 'fr'. Missing keys fall back to English.
Changelog¶
1.0.0¶
First release.