Explorer
KNOW-PAT-081

FiveM - Systeme medical avance : Anatomie, degats localises et soins progressifs

Domaine
fivem
Type
pattern
Priorité
P2

Parent : [[INDEX-FIVEM-GAMEPLAY]]

FiveM - Systeme medical avance : Anatomie, degats localises et soins progressifs

Probleme

Un systeme medical dans un serveur RP FiveM doit gerer : la localisation des degats (tete, bras, jambe, torse), les niveaux de blessure par arme, l'etat de conscience (mort/inconscient), le brancard/transport, et les soins progressifs. Le tout en server-authoritative pour eviter le godmode client-side.

Contexte

Basé sur les patterns extraits de wasabi-ambulance et vms_documents :

  • Mapping os GTA → parties du corps (31085 = head, 45509 = left_arm, ...)
  • Table de degats par arme (WEAPON_PISTOL = 1, ...)
  • Systeme UI de mort configurable (5 styles)
  • Brancard avance avec offsets position/rotation
  • Gestion d'etat medical (saignement, fracture, inconscience)
  • Police peut traiter les blesses (configurable)

Solution

1. Mapping anatomique server-side

Anatomy = {
    bones = {
        [31085] = { part = 'head',     critical = true,  effect = 'instant_knockout' },
        [45509] = { part = 'left_arm',  critical = false, effect = 'aim_shake' },
        [61163] = { part = 'right_arm', critical = false, effect = 'weapon_drop' },
        [58271] = { part = 'left_leg',  critical = false, effect = 'limp_slow' },
        [63931] = { part = 'right_leg', critical = false, effect = 'limp_slow' },
        [51826] = { part = 'torso',     critical = true,  effect = 'bleeding_heavy' },
        [36864] = { part = 'pelvis',    critical = false, effect = 'limp_slow' },
    },
    damageLevels = {
        [1] = { label = 'Leger',   bleedRate = 0.1, recovery = 30 },
        [2] = { label = 'Modere',  bleedRate = 0.3, recovery = 60 },
        [3] = { label = 'Grave',   bleedRate = 0.6, recovery = 120 },
        [4] = { label = 'Critique', bleedRate = 1.0, recovery = 300 },
    },
}

2. Degats par arme (server-side)

WeaponDamageTable = {
    [`WEAPON_PISTOL`] = { base = 2, headMultiplier = 4.0, limbMultiplier = 0.5 },
    [`WEAPON_CARBINERIFLE`] = { base = 3, headMultiplier = 5.0, limbMultiplier = 0.7 },
    [`WEAPON_KNIFE`] = { base = 3, headMultiplier = 2.0, limbMultiplier = 0.8, bleedBonus = 0.5 },
}

RegisterNetEvent('medical:registerDamage')
AddEventHandler('medical:registerDamage', function(weaponHash, boneId, damageReported)
    local src = source
    local weaponData = WeaponDamageTable[weaponHash] or { base = 1, headMultiplier = 2.0, limbMultiplier = 0.5 }
    local boneData = Anatomy.bones[boneId] or { part = 'torso', critical = false }
    
    local multiplier = boneData.critical and weaponData.headMultiplier or weaponData.limbMultiplier
    local finalDamage = weaponData.base * multiplier
    local bleedRate = Anatomy.damageLevels[math.min(finalDamage, 4)].bleedRate
    
    local currentState = Player(src).state.medicalState or { injuries = {}, blood = 100 }
    table.insert(currentState.injuries, {
        part = boneData.part,
        level = math.min(finalDamage, 4),
        bleedRate = bleedRate,
        time = os.time()
    })
    
    currentState.blood = math.max(currentState.blood - (bleedRate * 10), 0)
    Player(src).state:set('medicalState', currentState, true)
end)

3. Saignement progressif (server-side tick)

Citizen.CreateThread(function()
    while true do
        Citizen.Wait(1000)
        for _, playerId in ipairs(GetPlayers()) do
            local medicalState = Player(playerId).state.medicalState
            if medicalState and medicalState.injuries then
                local totalBleed = 0
                for _, injury in ipairs(medicalState.injuries) do
                    totalBleed = totalBleed + injury.bleedRate
                end
                if totalBleed > 0 then
                    medicalState.blood = math.max(medicalState.blood - totalBleed, 0)
                    Player(playerId).state:set('medicalState', medicalState, true)
                    if medicalState.blood <= 0 then
                        Player(playerId).state:set('isDead', true, true)
                        TriggerClientEvent('medical:onDeath', playerId)
                    end
                end
            end
        end
    end
end)

4. Brancard avec physique server-side

local Stretchers = {}

function Stretcher:create(stretcherId, ownerId)
    local ownerPed = GetPlayerPed(ownerId)
    local ownerPos = GetEntityCoords(ownerPed)
    
    self[stretcherId] = {
        owner = ownerId,
        position = ownerPos,
        attachedPatient = nil,
        offsets = Config.StretcherOffsets or {
            attach = vector3(0.0, 0.0, 0.95),
            carry = vector3(0.0, -0.5, 0.0),
        }
    }
    TriggerClientEvent('stretcher:spawned', -1, stretcherId, self[stretcherId])
end

RegisterNetEvent('stretcher:updatePosition')
AddEventHandler('stretcher:updatePosition', function(stretcherId, pos, rot)
    local src = source
    local stretcher = Stretchers[stretcherId]
    if not stretcher or stretcher.owner ~= src then return end
    
    local maxMove = 10.0
    if #(pos - stretcher.position) > maxMove then return end
    
    stretcher.position = pos
    stretcher.rotation = rot
    TriggerClientEvent('stretcher:syncPosition', -1, stretcherId, pos, rot)
end)

5. Soins progressifs avec timer + distance check

function MedicalSystem:startTreatment(medicId, patientId, treatmentType)
    if not self:canTreat(medicId, patientId) then return false end
    
    local treatmentConfig = Config.Treatments[treatmentType]
    if not treatmentConfig then return false, 'Traitement inconnu' end
    
    TriggerClientEvent('medical:startProgress', medicId, {
        duration = treatmentConfig.duration,
        label = ('Soigner %s (%s)'):format(treatmentConfig.targetPart, treatmentConfig.label),
        canCancel = true,
    })
    
    SetTimeout(treatmentConfig.duration, function()
        local medicPed = GetPlayerPed(medicId)
        local patientPed = GetPlayerPed(patientId)
        if not medicPed or not patientPed then
            TriggerClientEvent('medical:treatmentFailed', medicId, 'Joueur deconnecte')
            return
        end
        if #(GetEntityCoords(medicPed) - GetEntityCoords(patientPed)) > 3.0 then
            TriggerClientEvent('medical:treatmentFailed', medicId, 'Patient trop loin')
            return
        end
        self:applyTreatment(patientId, treatmentType)
        TriggerClientEvent('medical:treatmentSuccess', medicId, treatmentType)
    end)
    
    return true
end

6. Police peut traiter (configurable)

Config.CanPoliceTreat = true
Config.PoliceTreatmentLevel = 2

function MedicalSystem:canTreat(medicId, patientId)
    local medicJob = self:getPlayerJob(medicId)
    if medicJob == 'ambulance' then return true end
    if medicJob == 'police' and Config.CanPoliceTreat then return true end
    if medicId == patientId then
        local state = Player(medicId).state.medicalState
        if state then
            local maxInjury = 0
            for _, injury in ipairs(state.injuries) do
                maxInjury = math.max(maxInjury, injury.level)
            end
            return maxInjury <= 1
        end
    end
    return false
end

Checklist de validation

  • Mapping anatomique server-side (pas de trust du client)
  • Degats par arme calcules server-side
  • Saignement progressif gere par un tick serveur
  • Knockout/mort decides par le serveur
  • Brancard avec physique server-side (position validee)
  • Soins avec timer + distance check server-side
  • Police peut traiter configurable
  • Progress bar/action configurable (duree, animation, cancellable)
  • Logs d'audit pour soins et morts
  • Pas de godmode possible cote client
  • Recuperation naturelle progressive (configurable)

References

  • Patterns source : wasabi-ambulance, vms_documents
  • FiveM Native : GetEntityCoords, GetPedLastDamageBone, GetWeaponDamage