```lua
--[[
    Enhanced Ragdoll Physics Script for Garry's Mod
    Features:
    - Hitgroup-Based Force Application
    - Custom Ragdoll Models
    - Physics Constraints with Enhanced Validations
    - Environmental Interactions
]]
-- Таблица для хранения последней информации о повреждениях для сущностей
local EntityLastDamage = {}
-- Определение констант типов урона
local DAMAGE_TYPE = {
    BULLET = DMG_BULLET,
    BLAST = DMG_BLAST,
    SLASH = DMG_SLASH,
    FIRE = DMG_BURN,
    CHEMICAL = DMG_ACID,
    EXPLOSION = DMG_BLAST_SURFACE,
    FALL = DMG_FALL,
    GENERIC = DMG_GENERIC,
}

-- Определение констант групп попаданий
local HITGROUP_HEAD = 0
local HITGROUP_CHEST = 1
local HITGROUP_STOMACH = 2
local HITGROUP_LEFTARM = 3
local HITGROUP_RIGHTARM = 4
local HITGROUP_LEFTLEG = 5
local HITGROUP_RIGHTLEG = 6
local HITGROUP_GENERIC = 7

-- Таблица для отслеживания созданных ragdoll-ов, чтобы избежать дубликатов
local CreatedRagdolls = {}

-- Конфигурация для приложения силы на основе групп попаданий
local HITGROUP_FORCE_MULTIPLIERS = {
    [HITGROUP_HEAD] = {
        force = 1000,
        torque = 500
    },
    [HITGROUP_CHEST] = {
        force = 800,
        torque = 400
    },
    [HITGROUP_STOMACH] = {
        force = 600,
        torque = 300
    },
    [HITGROUP_LEFTARM] = {
        force = 500,
        torque = 250
    },
    [HITGROUP_RIGHTARM] = {
        force = 500,
        torque = 250
    },
    [HITGROUP_LEFTLEG] = {
        force = 400,
        torque = 200
    },
    [HITGROUP_RIGHTLEG] = {
        force = 400,
        torque = 200
    },
    -- Значения по умолчанию для неопределённых групп попаданий
    [HITGROUP_GENERIC] = {
        force = 700,
        torque = 350
    },
}

-- Конфигурация для кастомных ragdoll-моделей
local CUSTOM_RAGDOLL_MODELS = {
    ["models/player/kleiner.mdl"] = "models/player/kleiner_ragdoll.mdl", -- Убедитесь, что эта модель существует
    ["models/player/male_01.mdl"] = "models/player/male_ragdoll.mdl",      -- Убедитесь, что эта модель существует
    -- Добавьте другие соответствующие модели по необходимости
}

-- Список названий костей для создания констрейнтов (оптимизируйте под вашу модель)
local CONSTRAIN_BONE_NAMES = {
    "ValveBiped.Bip01_Spine",       -- Спина
    "ValveBiped.Bip01_Pelvis",      -- Таз
    "ValveBiped.Bip01_R_UpperArm",  -- Правая верхняя рука
    "ValveBiped.Bip01_L_UpperArm",  -- Левая верхняя рука
    "ValveBiped.Bip01_R_Forearm",   -- Правая предплечье
    "ValveBiped.Bip01_L_Forearm",   -- Левая предплечье
    "ValveBiped.Bip01_R_Hand",      -- Правая рука
    "ValveBiped.Bip01_L_Hand",      -- Левая рука
    "ValveBiped.Bip01_R_Thigh",     -- Правое бедро
    "ValveBiped.Bip01_L_Thigh",     -- Левое бедро
    "ValveBiped.Bip01_R_Calf",      -- Правое голень
    "ValveBiped.Bip01_L_Calf",      -- Левое голень
    "ValveBiped.Bip01_R_Foot",      -- Правая нога
    "ValveBiped.Bip01_L_Foot",      -- Левая нога
    -- "ValveBiped.Bip01_Head",    -- Голова (закомментируйте, если отсутствует)
    -- Добавьте другие важные кости по необходимости
}

-- Минимальное расстояние между костями для создания констрейнта
local MIN_BONE_DISTANCE = 4 -- Уменьшено с 5 до 4 для допуска близких костей

-- Хук для сохранения последней информации о повреждении при получении урона сущностью
hook.Add("EntityTakeDamage", "StoreEntityLastDamageType", function(target, dmginfo)
    if not IsValid(target) then return end
    if target:IsPlayer() or target:IsNPC() then
        local hitgroup = HITGROUP_GENERIC -- Группа попадания по умолчанию
        -- Попытка определить группу попадания только для игроков
        if target:IsPlayer() and dmginfo.GetHitGroup and isfunction(dmginfo.GetHitGroup) then
            hitgroup = dmginfo:GetHitGroup() or HITGROUP_GENERIC
        end
        -- Сохранение информации о повреждении
        EntityLastDamage[target:EntIndex()] = {
            dmgType = dmginfo:GetDamageType(),
            attacker = dmginfo:GetAttacker(),
            damagePos = dmginfo:GetDamagePosition() or target:GetPos(),
            hitgroup = hitgroup
        }
    end
end)

-- Утилитная функция для получения кастомной ragdoll-модели, если она сопоставлена
local function GetCustomRagdollModel(originalModel)
    return CUSTOM_RAGDOLL_MODELS[originalModel] or originalModel
end

-- Утилитная функция для определения силы и момента на основе группы попадания
local function GetForceByHitgroup(hitgroup)
    local config = HITGROUP_FORCE_MULTIPLIERS[hitgroup] or HITGROUP_FORCE_MULTIPLIERS[HITGROUP_GENERIC]
    return config.force, config.torque
end

-- Функция для получения родительской кости для заданной кости
local function GetParentBone(ent, boneIndex)
    if not IsValid(ent) then return -1 end
    return ent:GetBoneParent(boneIndex)
end

-- Функция для безопасного получения позиции кости
local function GetBoneData(rag, boneIndex)
    local boneCount = rag:GetBoneCount()
    if boneIndex < 0 or boneIndex >= boneCount then
        print(string.format("Error: Bone index %d is out of range for model '%s'.", boneIndex, rag:GetModel()))
        return nil
    end

    local pos = rag:GetBonePosition(boneIndex)
    if not pos then
        print(string.format("Error: Unable to get position for bone %d (%s).", boneIndex, rag:GetBoneName(boneIndex) or "Unknown"))
        return nil
    end

    return pos
end

-- Функция для получения индексов костей по их названиям
local function GetConstrainBoneIndices(rag)
    local boneIndices = {}
    for _, boneName in ipairs(CONSTRAIN_BONE_NAMES) do
        local index = rag:LookupBone(boneName)
        if index then
            table.insert(boneIndices, index)
        else
            print(string.format("Warning: Bone with name '%s' not found in model '%s'.", boneName, rag:GetModel()))
        end
    end
    return boneIndices
end

-- Функция для создания реалистичного ragdoll-а с улучшенной физикой
local function CreateRealisticRagdoll(victim, position, angles, model, dmgInfo)
    if not IsValid(victim) then return end
    local ragdollModel = GetCustomRagdollModel(model)
    local rag = ents.Create("prop_ragdoll")
    if not IsValid(rag) then
        print("Error: Failed to create ragdoll entity.")
        return
    end

    rag:SetModel(ragdollModel)
    rag:SetPos(position)
    rag:SetAngles(angles)
    rag:SetNWBool("IsCustomRagdoll", true)
    rag:Spawn()
    -- Опционально: Установка уникального имени для ragdoll-а
    rag:SetName("CustomRagdoll_" .. rag:EntIndex())

    -- Расчёт направления силы на основе позиции урона
    local forceDir = Vector(0, 0, 1) -- Сила вверх по умолчанию
    if victim:GetPos() ~= dmgInfo.damagePos then
        forceDir = (rag:GetPos() - dmgInfo.damagePos):GetNormalized()
    elseif IsValid(dmgInfo.attacker) then
        forceDir = (rag:GetPos() - dmgInfo.attacker:GetPos()):GetNormalized()
    end

    -- Определение величины силы и момента на основе группы попадания
    local forceMagnitude, torqueMagnitude = GetForceByHitgroup(dmgInfo.hitgroup)

    -- Корректировка вектора силы на основе типа урона
    local forceVector = Vector(0, 0, 0)
    if bit.band(dmgInfo.dmgType, DAMAGE_TYPE.EXPLOSION) ~= 0 then
        forceVector = forceDir * (forceMagnitude * 1.2) + Vector(0, 0, 500)
    elseif bit.band(dmgInfo.dmgType, DAMAGE_TYPE.BLAST) ~= 0 then
        forceVector = forceDir * forceMagnitude + Vector(0, 0, 400)
    elseif bit.band(dmgInfo.dmgType, DAMAGE_TYPE.BULLET) ~= 0 then
        forceVector = forceDir * forceMagnitude + Vector(0, 0, 450)
    elseif bit.band(dmgInfo.dmgType, DAMAGE_TYPE.FIRE) ~= 0 or bit.band(dmgInfo.dmgType, DAMAGE_TYPE.CHEMICAL) ~= 0 then
        forceVector = Vector(0, 0, forceMagnitude)
    elseif bit.band(dmgInfo.dmgType, DAMAGE_TYPE.SLASH) ~= 0 then
        forceVector = forceDir * forceMagnitude + Vector(0, 0, 350)
    else
        forceVector = Vector(0, 0, forceMagnitude)
    end

    -- Генерация случайного момента на основе величины момента
    local torqueVector = Vector(math.Rand(-torqueMagnitude, torqueMagnitude), math.Rand(-torqueMagnitude, torqueMagnitude), math.Rand(-torqueMagnitude, torqueMagnitude))
    -- Применение силы и момента с небольшим случайным задержкой для реалистичности
    timer.Simple(0.05, function()
        if not IsValid(rag) then return end
        for i = 0, rag:GetPhysicsObjectCount() - 1 do
            local phys = rag:GetPhysicsObjectNum(i)
            if IsValid(phys) then
                -- Применение центральной силы с небольшими вариациями
                local variation = Vector(math.Rand(-30, 30), math.Rand(-30, 30), math.Rand(-30, 30))
                phys:ApplyForceCenter(forceVector + variation)
                -- Применение момента для вращательного движения
                phys:AddAngleVelocity(torqueVector)
                -- Регулировка отдельных физических свойств для большей реалистичности
                phys:SetMass(math.Clamp(phys:GetMass(), 5, 100)) -- Ограничение массы до реалистичного диапазона
                phys:EnableGravity(true)
                phys:EnableDrag(true)
            end
        end
    end)

    -- Настройка физического констрейнта для структурной целостности
    timer.Simple(0.1, function()
        if not IsValid(rag) then return end
        local boneIndices = GetConstrainBoneIndices(rag)
        if #boneIndices == 0 then
            print("Error: No valid bones found for constraints.")
            return
        end

        for _, boneIndex in ipairs(boneIndices) do
            local parentBone = GetParentBone(rag, boneIndex)
            if parentBone == -1 then
                print(string.format("Error: Кость %d (%s) не имеет допустимого родителя.", boneIndex, rag:GetBoneName(boneIndex) or "Unknown"))
                -- Переход к следующей итерации цикла
            elseif not table.HasValue(boneIndices, parentBone) then
                print(string.format("Warning: Родительская кость %d (%s) для кости %d (%s) не в списке ограничений.",
                    parentBone, rag:GetBoneName(parentBone) or "Unknown",
                    boneIndex, rag:GetBoneName(boneIndex) or "Unknown"))
                -- Переход к следующей итерации цикла
            else
                local pos1 = GetBoneData(rag, parentBone)
                local pos2 = GetBoneData(rag, boneIndex)

                if not pos1 or not pos2 then
                    print(string.format("Error: Неверные позиции для костей %d (%s) и/или %d (%s).",
                        parentBone, rag:GetBoneName(parentBone) or "Unknown",
                        boneIndex, rag:GetBoneName(boneIndex) or "Unknown"))
                    -- Переход к следующей итерации цикла
                else
                    -- Проверка минимального расстояния между костями
                    local distance = pos1:Distance(pos2)
                    if distance <= MIN_BONE_DISTANCE then
                        print(string.format("Error: Кости %d (%s) и %d (%s) слишком близко друг к другу (Расстояние: %.2f).",
                            parentBone, rag:GetBoneName(parentBone) or "Unknown",
                            boneIndex, rag:GetBoneName(boneIndex) or "Unknown",
                            distance))
                        -- Переход к следующей итерации цикла
                    else
                        local constraintPos = (pos1 + pos2) / 2
                        local localPos = rag:WorldToLocal(constraintPos)

                        -- Создание шарнирного констрейнта (Ballsocket)
                        local constraintEntity = constraint.Ballsocket(
                            rag,          -- Entity1
                            rag,          -- Entity2 (та же сущность для костей внутри ragdoll)
                            parentBone,   -- Bone1
                            boneIndex,    -- Bone2
                            localPos,     -- Position
                            15,           -- Forcelimit
                            0,            -- Torquelimit
                            0             -- Hangs off angle (опционально)
                        )

                        if not IsValid(constraintEntity) then
                            print(string.format("Error: Не удалось создать Ballsocket между костями %d (%s) и %d (%s).",
                                parentBone, rag:GetBoneName(parentBone) or "Unknown",
                                boneIndex, rag:GetBoneName(boneIndex) or "Unknown"))
                        else
                            print(string.format("Ballsocket создан между костями %d (%s) и %d (%s) на локальной позиции %s.",
                                parentBone, rag:GetBoneName(parentBone) or "Unknown",
                                boneIndex, rag:GetBoneName(boneIndex) or "Unknown",
                                tostring(localPos)))
                        end
                    end
                end
            end
        end
    end)

    -- Обработка взаимодействий с окружением
    -- Обеспечивает корректное столкновение ragdoll-а с миром и другими сущностями
    rag:SetCollisionGroup(COLLISION_GROUP_PASSABLE_DOOR)
    rag:PhysicsInit(SOLID_VPHYSICS)
    rag:SetMoveType(MOVETYPE_VPHYSICS)
    rag:SetSolid(SOLID_VPHYSICS)
    -- Автоматическое удаление ragdoll-а после длительного периода для оптимизации производительности
    timer.Simple(600, function()
        if IsValid(rag) then
            rag:Remove()
        end
    end)
end

-- Функция для получения информации о повреждении с значениями по умолчанию
local function GetDamageInfo(ent)
    return EntityLastDamage[ent:EntIndex()] or {
        dmgType = DAMAGE_TYPE.GENERIC,
        attacker = nil,
        damagePos = ent:GetPos(),
        hitgroup = HITGROUP_GENERIC
    }
end

-- Функция для удаления стандартных ragdoll-ов и замены их на кастомные
local function RemoveDefaultRagdoll(pos)
    local closestRagdoll = nil
    local minDistance = 100
    for _, ent in ipairs(ents.FindByClass("prop_ragdoll")) do
        if not ent:GetNWBool("IsCustomRagdoll", false) then
            local distance = ent:GetPos():Distance(pos)
            if distance < minDistance then
                closestRagdoll = ent
                minDistance = distance
            end
        end
    end

    if IsValid(closestRagdoll) then
        closestRagdoll:Remove()
    end
end

-- Хук для обработки смертей игроков
hook.Add("PlayerDeath", "CreateRealisticCorpse_Player", function(victim, inflictor, attacker)
    if not IsValid(victim) then return end
    if CreatedRagdolls[victim:EntIndex()] then return end
    CreatedRagdolls[victim:EntIndex()] = true
    local model = victim:GetModel()
    local pos = victim:GetPos()
    local ang = victim:EyeAngles()
    local dmgInfo = GetDamageInfo(victim)
    CreateRealisticRagdoll(victim, pos, ang, model, dmgInfo)
    -- Удаление стандартного ragdoll-а вскоре после создания кастомного
    timer.Simple(0.1, function()
        if not IsValid(victim) then return end
        RemoveDefaultRagdoll(pos)
    end)
end)

-- Хук для обработки смертей NPC
hook.Add("OnNPCKilled", "CreateRealisticCorpse_NPC", function(npc, attacker, inflictor)
    if not IsValid(npc) then return end
    if CreatedRagdolls[npc:EntIndex()] then return end
    CreatedRagdolls[npc:EntIndex()] = true
    local model = npc:GetModel()
    local pos = npc:GetPos()
    local ang = npc:GetAngles()
    local dmgInfo = GetDamageInfo(npc)
    CreateRealisticRagdoll(npc, pos, ang, model, dmgInfo)
    -- Удаление стандартного ragdoll-а вскоре после создания кастомного
    timer.Simple(0.1, function()
        if not IsValid(npc) then return end
        RemoveDefaultRagdoll(pos)
    end)
end)

-- Хук для очистки данных о повреждениях при удалении сущностей
hook.Add("OnEntityRemoved", "CleanUpEntityDamageData", function(ent)
    if not IsValid(ent) then return end
    EntityLastDamage[ent:EntIndex()] = nil
    CreatedRagdolls[ent:EntIndex()] = nil
end)

-- Хук для очистки данных при отключении игроков
hook.Add("PlayerDisconnected", "CleanUpPlayerDamageData", function(ply)
    if not IsValid(ply) then return end
    EntityLastDamage[ply:EntIndex()] = nil
    CreatedRagdolls[ply:EntIndex()] = nil
end)
```
Раздели код на несколько файлов для аддона, напиши структуру проекта.