require "MS/MS_NoxiousExposure"
MS_PlayerStatus = {}
MS_PlayerStatus.Traits = {
    highIsBetter = {
        ["MS_EnduranceIndicator"] = true,      -- Endurance
        ["MS_HealthIndicator"] = true,         -- Health
        ["MS_HungerIndicator"] = true,         -- SHunger
        ["MS_CalorieIndicator"] = true,        -- Calorie
        ["MS_LipidsIndicator"] = true,         -- Lipids
        ["MS_ProteinsIndicator"] = true,       -- Proteins
        ["MS_ThirstIndicator"] = true,         -- Thirst
        ["MS_CarbohydratesIndicator"] = true,  -- Carbohydrates
        ["MS_MedicineIndicator"] = true,  -- Medicine
        ["MS_StiffnessIndicator"] = false,      -- Stiffness
        ["MS_NoxiousExposureIndicator"] = false, -- Noxious exposure

    }
}
MS_PlayerStatus.isHigherBetter = function(indicatorType)
    return MS_PlayerStatus.Traits.highIsBetter[indicatorType] == true
end

-- ---------------------------------------------------------------------------------------- --
-- CalcStatus
-- ---------------------------------------------------------------------------------------- --
MS_PlayerStatus.Calc = {}

MS_PlayerStatus.Calc.Thirst = function(value)
    return 1 - value
end

MS_PlayerStatus.Calc.Hunger = function(value)
    return 1 - value
end

MS_PlayerStatus.Calc.Endurance = function(value)
    return value
end

MS_PlayerStatus.Calc.Fatigue = function(value)
    return value
end

MS_PlayerStatus.Calc.Temperature = function(value)
    local maxTempLim = 42
    local minTempLim = 20
    return (value - minTempLim) / (maxTempLim - minTempLim)
end

MS_PlayerStatus.Calc.Panic = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Stress = function(value)
    if value > 1 then value = 1 end
    return value
end

MS_PlayerStatus.Calc.BoredomLevel = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.UnhappynessLevel = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.DiscomfortLevel = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Health = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Sickness = function(value)
    return value
end

MS_PlayerStatus.Calc.Poison = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.ColdStrength = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Calorie = function(value)
    local maxCalorie = 3700
    local minCalorie = -2200
    return (value - minCalorie) / (maxCalorie - minCalorie)
end

MS_PlayerStatus.Calc.Pain = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Bleeding = function(value)
    return math.max(0, math.min(4, tonumber(value) or 0))
end

MS_PlayerStatus.Calc.CarryWeight = function(weight, capacity)
    local safeWeight = math.max(0, tonumber(weight) or 0)
    local safeCapacity = math.max(0, tonumber(capacity) or 0)

    if safeCapacity <= 0 then
        return 0
    end

    -- CarryWeight uses a fixed visual scale of 0 .. 2 * capacity.
    local visualMax = safeCapacity * 2
    if visualMax <= 0 then
        return 0
    end

    return math.max(0, math.min(1, safeWeight / visualMax))
end

MS_PlayerStatus.Calc.Proteins = function(value)
    return (value + 500) / 1500
end

MS_PlayerStatus.Calc.Carbohydrates = function(value)
    return (value + 500) / 1500
end

MS_PlayerStatus.Calc.Lipids = function(value)
    return (value + 500) / 1500
end

MS_PlayerStatus.Calc.Weight = function(value)
    local maxWeight = 130
    local minWeight = 35
    return (value - minWeight) / (maxWeight - minWeight)
end

MS_PlayerStatus.Calc.Dirtiness = function(value)
    return value
end

MS_PlayerStatus.Calc.Bloodiness = function(value)
    return value
end

MS_PlayerStatus.Calc.Windchill = function(value)
    return value
end

MS_PlayerStatus.Calc.Anger = function(value)
    return value
end

MS_PlayerStatus.Calc.Wetness = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Drunkenness = function(value)
    return value / 100
end

MS_PlayerStatus.Calc.Stiffness = function(value)
    return value / 100
end

local function MS_PlayerStatusResolveMoodleType(newName, legacyName)
    if not MoodleType then
        return nil
    end

    return MoodleType[newName] or MoodleType[legacyName]
end

local function MS_PlayerStatusGetMoodleLevel(player, newName, legacyName)
    if not player or not player.getMoodles then
        return 0
    end

    local moodleType = MS_PlayerStatusResolveMoodleType(newName, legacyName)
    if not moodleType then
        return 0
    end

    local moodles = player:getMoodles()
    if not moodles or not moodles.getMoodleLevel then
        return 0
    end

    local ok, level = pcall(function()
        return moodles:getMoodleLevel(moodleType)
    end)

    if ok then
        return math.max(0, math.min(4, tonumber(level) or 0))
    end

    return 0
end

local function MS_PlayerStatusGetBleedingData(player)
    local result = {
        level = 0,
        bleedingParts = 0,
        neckBleeding = false,
    }

    if not player or player:isDead() or not player.getBodyDamage then
        return result
    end

    local bodyDamage = player:getBodyDamage()
    if not bodyDamage then
        return result
    end

    local moodleLevel = MS_PlayerStatusGetMoodleLevel(player, "BLEEDING", "Bleeding")
    local bodyParts = bodyDamage.getBodyParts and bodyDamage:getBodyParts() or nil

    if bodyParts then
        for i = 0, bodyParts:size() - 1 do
            local bodyPart = bodyParts:get(i)
            local bleedingTime = bodyPart and bodyPart.getBleedingTime and (tonumber(bodyPart:getBleedingTime()) or 0) or 0
            local isBandaged = bodyPart and bodyPart.bandaged and bodyPart:bandaged() == true
            local activelyBleeding = false

            if bodyPart and bodyPart.bleeding then
                activelyBleeding = bodyPart:bleeding() == true
            else
                activelyBleeding = bleedingTime > 0 and not isBandaged
            end

            -- Vanilla clears the Bleeding moodle when all bleeding wounds are
            -- bandaged. Count only active, unbandaged bleeding here so the
            -- Modern Status indicator matches the vanilla moodle state.
            if activelyBleeding then
                result.bleedingParts = result.bleedingParts + 1

                -- Neck bleeding is especially dangerous in vanilla and maps to
                -- the highest bleeding moodle level. Keep this check defensive
                -- because enum names differ between B42 point releases.
                local partType = bodyPart.getType and bodyPart:getType() or nil
                if BodyPartType and (partType == BodyPartType.Neck or partType == BodyPartType.NECK) then
                    result.neckBleeding = true
                elseif partType and tostring(partType):lower():find("neck", 1, true) then
                    result.neckBleeding = true
                end
            end
        end
    end

    result.level = moodleLevel

    return result
end

-- ---------------------------------------------------------------------------------------- --
-- GetStatus
-- ---------------------------------------------------------------------------------------- --
MS_PlayerStatus.Get = {}

MS_PlayerStatus.Get.Thirst = function(player)
    if player:isDead() then
        return -1
    else
        return math.max(0, math.min(1, MS_PlayerStatus.Calc.Thirst(player:getStats():get(CharacterStat.THIRST))))
    end
end

MS_PlayerStatus.Get.Hunger = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Hunger(player:getStats():get(CharacterStat.HUNGER))
    end
end

MS_PlayerStatus.Get.Endurance = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Endurance(player:getStats():get(CharacterStat.ENDURANCE))
    end
end

MS_PlayerStatus.Get.Fatigue = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Fatigue(player:getStats():get(CharacterStat.FATIGUE))
    end
end

MS_PlayerStatus.Get.Temperature = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Temperature(player:getStats():get(CharacterStat.TEMPERATURE))
    end
end

MS_PlayerStatus.Get.Panic = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Panic(player:getStats():get(CharacterStat.PANIC))
    end
end

MS_PlayerStatus.Get.Stress = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Stress(player:getStats():getNicotineStress())
    end
end

MS_PlayerStatus.Get.BoredomLevel = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.BoredomLevel(player:getStats():get(CharacterStat.BOREDOM))
    end
end

MS_PlayerStatus.Get.UnhappynessLevel = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.UnhappynessLevel(player:getStats():get(CharacterStat.UNHAPPINESS))
    end
end

MS_PlayerStatus.Get.DiscomfortLevel = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.DiscomfortLevel(player:getStats():get(CharacterStat.DISCOMFORT))
    end
end

MS_PlayerStatus.Get.Health = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Health(player:getBodyDamage():getHealth())
    end
end

MS_PlayerStatus.Get.Sickness = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Sickness(player:getBodyDamage():getApparentInfectionLevel()/100 + player:getStats():get(CharacterStat.SICKNESS))
    end
end

MS_PlayerStatus.Get.CatchACold = function(player)
    if player:isDead() then
        return -1
    else
        local bodyDamage = player:getBodyDamage()
        if not bodyDamage then
            return 0
        end

        -- Use coldStrength first because the General Debugger slider
        -- ("Catch Cold Strength") is wired to getColdStrength/setColdStrength.
        if bodyDamage.getColdStrength then
            return math.max(0, math.min(1, MS_PlayerStatus.Calc.ColdStrength(bodyDamage:getColdStrength())))
        end

        -- Fallback kept for compatibility with older logic/state.
        if bodyDamage.getCatchACold then
            return math.max(0, math.min(1, MS_PlayerStatus.Calc.ColdStrength(bodyDamage:getCatchACold())))
        end

        return 0
    end
end

MS_PlayerStatus.Get.Poison = function(player)
    if player:isDead() then
        return -1
    else
        return math.max(0, math.min(1, MS_PlayerStatus.Calc.Poison(player:getStats():get(CharacterStat.POISON))))
    end
end

MS_PlayerStatus.Get.Calorie = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Calorie(player:getNutrition():getCalories())
    end
end

MS_PlayerStatus.Get.Pain = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Pain(player:getStats():get(CharacterStat.PAIN))
    end
end

MS_PlayerStatus.Get.BleedingData = function(player)
    return MS_PlayerStatusGetBleedingData(player)
end

MS_PlayerStatus.Get.Bleeding = function(player)
    if player:isDead() then
        return -1
    end

    return MS_PlayerStatus.Calc.Bleeding(MS_PlayerStatusGetBleedingData(player).level)
end


local function MS_CarryWeightSafeNumber(value)
    return math.max(0, tonumber(value) or 0)
end

local function MS_CarryWeightGetItemName(item)
    if not item then
        return "?"
    end

    -- Prefer the translated display name exposed by the item itself.
    if item.getName then
        local name = item:getName()
        if name and tostring(name) ~= "" then
            return tostring(name)
        end
    end

    if item.getFullType then
        return tostring(item:getFullType() or "?")
    end

    return "?"
end

local function MS_CarryWeightIsEquippedLike(player, item)
    if not player or not item then
        return false
    end

    -- Match IsoGameCharacter.getInventoryWeight(): equipped, fake-equipped, and
    -- hotbar-attached items use a reduced encumbrance contribution.
    if player.isEquipped and player:isEquipped(item) then
        return true
    end
    if item.isFakeEquipped and item:isFakeEquipped(player) then
        return true
    end
    if item.getAttachedSlot and item:getAttachedSlot() and item:getAttachedSlot() > -1 then
        return true
    end

    return false
end

local function MS_CarryWeightGetEffectiveItemWeight(player, item)
    if not item then
        return 0
    end

    local isEquipped = player and player.isEquipped and player:isEquipped(item)
    if item.getAttachedSlot and item:getAttachedSlot() and item:getAttachedSlot() > -1 and not isEquipped and item.getHotbarEquippedWeight then
        return MS_CarryWeightSafeNumber(item:getHotbarEquippedWeight())
    end

    if MS_CarryWeightIsEquippedLike(player, item) and item.getEquippedWeight then
        return MS_CarryWeightSafeNumber(item:getEquippedWeight())
    end

    if item.getUnequippedWeight then
        return MS_CarryWeightSafeNumber(item:getUnequippedWeight())
    end

    if item.getActualWeight then
        return MS_CarryWeightSafeNumber(item:getActualWeight())
    end

    return 0
end

local function MS_CarryWeightResolveMoodleType(newName, legacyName)
    if not MoodleType then
        return nil
    end

    -- B42.13+ uses uppercase registry enum names, while early B42 builds used PascalCase.
    return MoodleType[newName] or MoodleType[legacyName]
end

local function MS_CarryWeightGetMoodleLevel(player, moodleType)
    if not player or not moodleType or not player.getMoodles then
        return 0
    end

    local moodles = player:getMoodles()
    if not moodles or not moodles.getMoodleLevel then
        return 0
    end

    return math.max(0, tonumber(moodles:getMoodleLevel(moodleType)) or 0)
end

local function MS_CarryWeightPenaltyForLevel(level, penalties)
    level = math.max(0, math.min(4, math.floor(tonumber(level) or 0)))
    return penalties[level] or 0
end

local function MS_CarryWeightBuildCapacityEffects(player)
    local effects = {}
    local total = 0

    if not player or not MoodleType then
        return effects, total
    end

    -- These reducers mirror BodyDamage.UpdateStrength() in B42: hunger,
    -- thirst, sickness, bleeding, and injury moodle levels reduce max carry.
    local definitions = {
        { key = "IGUI_ModernStatus_Hunger", fallback = "Hunger", moodle = MS_CarryWeightResolveMoodleType("HUNGRY", "Hungry"), penalties = { [2] = 1, [3] = 2, [4] = 2 } },
        { key = "IGUI_ModernStatus_Thirst", fallback = "Thirst", moodle = MS_CarryWeightResolveMoodleType("THIRST", "Thirst"), penalties = { [2] = 1, [3] = 2, [4] = 2 } },
        { key = "IGUI_ModernStatus_Sickness", fallback = "Sickness", moodle = MS_CarryWeightResolveMoodleType("SICK", "Sick"), penalties = { [2] = 1, [3] = 2, [4] = 3 } },
        { key = "IGUI_ModernStatus_Bleeding", fallback = "Bleeding", moodle = MS_CarryWeightResolveMoodleType("BLEEDING", "Bleeding"), penalties = { [2] = 1, [3] = 1, [4] = 1 } },
        { key = "IGUI_ModernStatus_Injured", fallback = "Injured", moodle = MS_CarryWeightResolveMoodleType("INJURED", "Injured"), penalties = { [2] = 1, [3] = 2, [4] = 3 } },
    }

    for _, definition in ipairs(definitions) do
        local level = MS_CarryWeightGetMoodleLevel(player, definition.moodle)
        local penalty = MS_CarryWeightPenaltyForLevel(level, definition.penalties)
        if penalty > 0 then
            effects[#effects + 1] = {
                key = definition.key,
                fallback = definition.fallback,
                level = level,
                penalty = penalty,
            }
            total = total + penalty
        end
    end

    return effects, total
end

local function MS_CarryWeightBuildInventoryBreakdown(player)
    local breakdown = {
        equippedItemsWeight = 0,
        otherInventoryWeight = 0,
        equippedContainersGross = 0,
        equippedContainersReduced = 0,
        equippedContainers = {},
    }

    if not player or not player.getInventory then
        return breakdown
    end

    local inventory = player:getInventory()
    local items = inventory and inventory.getItems and inventory:getItems() or nil
    if not items or not items.size then
        return breakdown
    end

    for i = 0, items:size() - 1 do
        local item = items:get(i)
        if item then
            local isContainer = item.IsInventoryContainer and item:IsInventoryContainer()
            local equippedLike = MS_CarryWeightIsEquippedLike(player, item)
            local effectiveWeight = MS_CarryWeightGetEffectiveItemWeight(player, item)

            if isContainer and equippedLike then
                local grossWeight = item.getUnequippedWeight and MS_CarryWeightSafeNumber(item:getUnequippedWeight()) or effectiveWeight
                local reduction = item.getWeightReduction and MS_CarryWeightSafeNumber(item:getWeightReduction()) or 0

                breakdown.equippedContainersGross = breakdown.equippedContainersGross + grossWeight
                breakdown.equippedContainersReduced = breakdown.equippedContainersReduced + effectiveWeight
                breakdown.equippedContainers[#breakdown.equippedContainers + 1] = {
                    name = MS_CarryWeightGetItemName(item),
                    gross = grossWeight,
                    reduction = reduction,
                    reduced = effectiveWeight,
                }
            elseif equippedLike then
                breakdown.equippedItemsWeight = breakdown.equippedItemsWeight + effectiveWeight
            else
                breakdown.otherInventoryWeight = breakdown.otherInventoryWeight + effectiveWeight
            end
        end
    end

    return breakdown
end

MS_PlayerStatus.Get.CarryWeightData = function(player, includeDetails)
    if not player or player:isDead() then
        return {
            weight = 0,
            capacity = 0,
            loadRatio = 0,
            visualRatio = 0,
            equippedItemsWeight = 0,
            otherInventoryWeight = 0,
            equippedContainersGross = 0,
            equippedContainersReduced = 0,
            equippedContainers = {},
            capacityEffects = {},
            capacityPenaltyTotal = 0,
            baseCapacity = 0,
            strengthMultiplier = 1,
            traitMultiplier = 1,
        }
    end

    -- Fast path used by normal rendering. It avoids inventory iteration and
    -- moodle breakdown calculations unless detailed tooltip data is requested.
    local weight = MS_CarryWeightSafeNumber(player:getInventoryWeight())
    local capacity = MS_CarryWeightSafeNumber(player:getMaxWeight())
    local loadRatio = 0

    if capacity > 0 then
        loadRatio = weight / capacity
    end

    local data = {
        weight = weight,
        capacity = capacity,
        loadRatio = loadRatio,
        visualRatio = MS_PlayerStatus.Calc.CarryWeight(weight, capacity),
        equippedItemsWeight = 0,
        otherInventoryWeight = 0,
        equippedContainersGross = 0,
        equippedContainersReduced = 0,
        equippedContainers = {},
        capacityEffects = {},
        capacityPenaltyTotal = 0,
        baseCapacity = capacity,
        strengthMultiplier = 1,
        traitMultiplier = 1,
    }

    if includeDetails == true then
        local breakdown = MS_CarryWeightBuildInventoryBreakdown(player)
        local capacityEffects, penaltyTotal = MS_CarryWeightBuildCapacityEffects(player)

        data.equippedItemsWeight = breakdown.equippedItemsWeight
        data.otherInventoryWeight = breakdown.otherInventoryWeight
        data.equippedContainersGross = breakdown.equippedContainersGross
        data.equippedContainersReduced = breakdown.equippedContainersReduced
        data.equippedContainers = breakdown.equippedContainers
        data.capacityEffects = capacityEffects
        data.capacityPenaltyTotal = penaltyTotal
        data.baseCapacity = player.getMaxWeightBase and MS_CarryWeightSafeNumber(player:getMaxWeightBase()) or capacity
        data.strengthMultiplier = player.getWeightMod and (tonumber(player:getWeightMod()) or 1) or 1
        data.traitMultiplier = player.getMaxWeightDelta and (tonumber(player:getMaxWeightDelta()) or 1) or 1
    end

    return data
end

MS_PlayerStatus.Get.CarryWeight = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Get.CarryWeightData(player).visualRatio
    end
end

MS_PlayerStatus.Get.Proteins = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Proteins(player:getNutrition():getProteins())
    end
end

MS_PlayerStatus.Get.Carbohydrates = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Carbohydrates(player:getNutrition():getCarbohydrates())
    end
end

MS_PlayerStatus.Get.Lipids = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Lipids(player:getNutrition():getLipids())
    end
end

MS_PlayerStatus.Get.Weight = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Weight(player:getNutrition():getWeight())
    end
end

MS_PlayerStatus.Get.Dirtiness = function(player)
    if player:isDead() then
        return -1
    else
        local visual = player:getHumanVisual()
        local v = 0
        for i = 1, BloodBodyPartType.MAX:index() do
            local part = BloodBodyPartType.FromIndex(i - 1)
            v = v + visual:getBlood(part) + visual:getDirt(part)
        end
        return v / (BloodBodyPartType.MAX:index() * 2)
    end
end

MS_PlayerStatus.Get.Bloodiness = function(player)
    if player:isDead() then
        return -1
    else
        local visual = player:getHumanVisual()
        local v = 0
        for i = 1, BloodBodyPartType.MAX:index() do
            local part = BloodBodyPartType.FromIndex(i - 1)
            v = v + visual:getBlood(part)
        end
        return v / BloodBodyPartType.MAX:index()
    end
end

MS_PlayerStatus.Get.Windchill = function(player)
    if player:isDead() then
        return -1
    end

    if Temperature and Temperature.getWindChillAmountForPlayer then
        local ok, value = pcall(function()
            return Temperature.getWindChillAmountForPlayer(player)
        end)
        if ok then
            return math.max(0, tonumber(value) or 0)
        end
    end

    return 0
end


MS_PlayerStatus.Get.FoodHealingData = function(player)
    local data = {
        timer = 0,
        standard = 1600,
        maxTimer = 11000,
        level = 0,
        normalized = 0,
        irlSeconds = 0,
        gameMinutes = 0,
    }

    if not player or player:isDead() then
        return data
    end

    local bodyDamage = player:getBodyDamage()
    if not bodyDamage then
        return data
    end

    if bodyDamage.getHealthFromFoodTimer then
        local ok, value = pcall(function()
            return bodyDamage:getHealthFromFoodTimer()
        end)
        if ok then
            data.timer = math.max(0, tonumber(value) or 0)
        end
    end

    if bodyDamage.getStandardHealthFromFoodTime then
        local ok, value = pcall(function()
            return bodyDamage:getStandardHealthFromFoodTime()
        end)
        if ok and tonumber(value) and tonumber(value) > 0 then
            data.standard = tonumber(value)
        end
    end

    if data.timer > data.standard * 3 then
        data.level = 4
    elseif data.timer > data.standard * 2 then
        data.level = 3
    elseif data.timer > data.standard then
        data.level = 2
    elseif data.timer > 0 then
        data.level = 1
    end

    data.normalized = math.max(0, math.min(1, data.timer / data.maxTimer))

    -- BodyDamage decreases HealthFromFoodTimer by GameTime multiplier every update.
    -- At normal gameplay speed this is approximately 48 timer units per real-life second.
    -- Do not derive the displayed IRL time from GameTime:getMultiplier() here: in MP Host
    -- it can expose short-lived synchronization values that make the UI time jump up/down.
    data.irlSeconds = data.timer / 48

    local gameTime = nil
    if getGameTime then
        gameTime = getGameTime()
    end

    if gameTime and gameTime.getMinutesPerDay then
        local ok, value = pcall(function()
            return gameTime:getMinutesPerDay()
        end)
        local minutesPerDay = ok and tonumber(value) or 0
        if minutesPerDay and minutesPerDay > 0 then
            data.gameMinutes = data.timer / (minutesPerDay * 2)
        end
    end

    return data
end

MS_PlayerStatus.Get.FoodHealing = function(player)
    local data = MS_PlayerStatus.Get.FoodHealingData(player)
    return data and data.normalized or 0
end

MS_PlayerStatus.Get.Anger = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Anger(player:getStats():get(CharacterStat.ANGER))
    end
end

MS_PlayerStatus.Get.Wetness = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Wetness(player:getStats():get(CharacterStat.WETNESS))
    end
end

MS_PlayerStatus.Get.Drunkenness = function(player)
    if player:isDead() then
        return -1
    else
        return MS_PlayerStatus.Calc.Drunkenness(player:getStats():get(CharacterStat.INTOXICATION))
    end
end

MS_PlayerStatus.Get.Stiffness = function(player)
    if player:isDead() then
        return -1
    else
        -- only cout the Stiffness part
        local bodyDamage = player:getBodyDamage()
        local bodyParts = bodyDamage:getBodyParts()
        local totalStiffness = 0
        local stiffPartsCount = 0
        
        for i=0, bodyParts:size()-1 do
            local bodyPart = bodyParts:get(i)
            local stiffness = bodyPart:getStiffness()
            if stiffness > 0 then
                totalStiffness = totalStiffness + stiffness
                stiffPartsCount = stiffPartsCount + 1
            end
        end
        
        local avgStiffness = 0
        if stiffPartsCount > 0 then
            avgStiffness = totalStiffness / stiffPartsCount
        end
        
        return MS_PlayerStatus.Calc.Stiffness(avgStiffness)
    end
end

MS_PlayerStatus.Get.NoxiousExposureData = function(player)
    if not player or player:isDead() then
        return MS_NoxiousExposure.getData(nil)
    end

    return MS_NoxiousExposure.getData(player)
end

MS_PlayerStatus.Get.NoxiousExposure = function(player)
    if not player or player:isDead() then
        return 0
    end

    return MS_NoxiousExposure.getNormalizedValue(player)
end

return MS_PlayerStatus