require "MS/MS_StatusIndicator"

ISThirstStatusIndicator = MS_StatusIndicator:derive("ISThirstStatusIndicator")

function ISThirstStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Thirst"
    o.__type = "ISThirstStatusIndicator"
    return o
end

function ISThirstStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Thirst(self.player)
    end
    return 1
end

-- ---------------------------------------------------------------------------------------- --
-- Hunger Status
-- ---------------------------------------------------------------------------------------- --
ISHungerStatusIndicator = MS_StatusIndicator:derive("ISHungerStatusIndicator")

function ISHungerStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Hunger"
    o.__type = "ISHungerStatusIndicator"
    return o
end

function ISHungerStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Hunger(self.player)
    end
    return 1
end

-- ---------------------------------------------------------------------------------------- --
-- Endurance Status
-- ---------------------------------------------------------------------------------------- --
ISEnduranceStatusIndicator = MS_StatusIndicator:derive("ISEnduranceStatusIndicator")

function ISEnduranceStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_DifficultyBreathing"
    o.__type = "ISEnduranceStatusIndicator"
    return o
end

function ISEnduranceStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Endurance(self.player)
    end
    return 1
end

-- ---------------------------------------------------------------------------------------- --
-- Fatigue Status
-- ---------------------------------------------------------------------------------------- --
ISFatigueStatusIndicator = MS_StatusIndicator:derive("ISFatigueStatusIndicator")

function ISFatigueStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Sleepy"
    o.__type = "ISFatigueStatusIndicator"
    return o
end

function ISFatigueStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Fatigue(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Stress Status
-- ---------------------------------------------------------------------------------------- --
ISStressStatusIndicator = MS_StatusIndicator:derive("ISStressStatusIndicator")

function ISStressStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Stressed"
    o.__type = "ISStressStatusIndicator"
    return o
end

function ISStressStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Stress(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Panic Status
-- ---------------------------------------------------------------------------------------- --
ISPanicStatusIndicator = MS_StatusIndicator:derive("ISPanicStatusIndicator")

function ISPanicStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Panicked"
    o.__type = "ISPanicStatusIndicator"
    return o
end

function ISPanicStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Panic(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Temperature Status
-- ---------------------------------------------------------------------------------------- --
ISTemperatureStatusIndicator = MS_StatusIndicator:derive("ISTemperatureStatusIndicator")

function ISTemperatureStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "temperature_icon"
    o.useGradient = true
    o.__type = "ISTemperatureStatusIndicator"
    return o
end

function ISTemperatureStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Temperature(self.player)
    end
    return 0.5
end

function ISTemperatureStatusIndicator:getHoverText(value)
    local actualTemp = self.player:getBodyDamage():getTemperature()
    return string.format("%.1f", actualTemp)
end

-- ---------------------------------------------------------------------------------------- --
-- Windchill Status
-- ---------------------------------------------------------------------------------------- --
ISWindchillStatusIndicator = MS_StatusIndicator:derive("ISWindchillStatusIndicator")

function ISWindchillStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Windchill"
    o.preferModernStatusColorIcon = true
    o.__type = "ISWindchillStatusIndicator"
    return o
end

function ISWindchillStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Windchill(self.player)
    end
    return 0
end

function ISWindchillStatusIndicator:getHoverText(value)
    return string.format("%.1f C", tonumber(value) or 0)
end

-- ---------------------------------------------------------------------------------------- --
-- Health Status
-- ---------------------------------------------------------------------------------------- --
ISHealthStatusIndicator = MS_StatusIndicator:derive("ISHealthStatusIndicator")

function ISHealthStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "health_icon"
    o.__type = "ISHealthStatusIndicator"
    return o
end

function ISHealthStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Health(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Boredom Status
-- ---------------------------------------------------------------------------------------- --
ISBoredomStatusIndicator = MS_StatusIndicator:derive("ISBoredomStatusIndicator")

function ISBoredomStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Bored"
    o.__type = "ISBoredomStatusIndicator"
    return o
end

function ISBoredomStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.BoredomLevel(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Unhappyness Status
-- ---------------------------------------------------------------------------------------- --
ISUnhappynessStatusIndicator = MS_StatusIndicator:derive("ISUnhappynessStatusIndicator")

function ISUnhappynessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Sad"
    o.__type = "ISUnhappynessStatusIndicator"
    return o
end

function ISUnhappynessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.UnhappynessLevel(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Discomfort Status
-- ---------------------------------------------------------------------------------------- --
ISDiscomfortStatusIndicator = MS_StatusIndicator:derive("ISDiscomfortStatusIndicator")

function ISDiscomfortStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Discomfort"
    o.__type = "ISDiscomfortStatusIndicator"
    return o
end

function ISDiscomfortStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.DiscomfortLevel(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Sickness Status
-- ---------------------------------------------------------------------------------------- --
ISSicknessStatusIndicator = MS_StatusIndicator:derive("ISSicknessStatusIndicator")

function ISSicknessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Nauseous"
    o.__type = "ISSicknessStatusIndicator"
    return o
end

function ISSicknessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Sickness(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Catch A Cold Status
-- ---------------------------------------------------------------------------------------- --
ISCatchAColdStatusIndicator = MS_StatusIndicator:derive("ISCatchAColdStatusIndicator")

function ISCatchAColdStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Ill"
    o.__type = "ISCatchAColdStatusIndicator"
    return o
end

function ISCatchAColdStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.CatchACold(self.player)
    end
    return 0
end

function ISCatchAColdStatusIndicator:getHoverText(value)
    if not self.player or self.player:isDead() then
        return "0.0"
    end

    local bodyDamage = self.player:getBodyDamage()
    if not bodyDamage then
        return "0.0"
    end

    -- Match the value source used by the indicator itself.
    if bodyDamage.getColdStrength then
        return string.format("%.1f", bodyDamage:getColdStrength())
    end

    if bodyDamage.getCatchACold then
        return string.format("%.1f", bodyDamage:getCatchACold())
    end

    return "0.0"
end

-- ---------------------------------------------------------------------------------------- --
-- Poison Status
-- ---------------------------------------------------------------------------------------- --
ISPoisonStatusIndicator = MS_StatusIndicator:derive("ISPoisonStatusIndicator")

function ISPoisonStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Poison"
    o.__type = "ISPoisonStatusIndicator"
    return o
end

function ISPoisonStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Poison(self.player)
    end
    return 0
end

function ISPoisonStatusIndicator:getHoverText(value)
    local actualPoison = self.player:getStats():get(CharacterStat.POISON)
    return string.format("%.1f", actualPoison)
end

-- ---------------------------------------------------------------------------------------- --
-- Calorie Status
-- ---------------------------------------------------------------------------------------- --
ISCalorieStatusIndicator = MS_StatusIndicator:derive("ISCalorieStatusIndicator")

function ISCalorieStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "calorie_icon"
    o.__type = "ISCalorieStatusIndicator"
    return o
end

function ISCalorieStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Calorie(self.player)
    end
    return 1
end

function ISCalorieStatusIndicator:getHoverText(value)
    local actualCalories = self.player:getNutrition():getCalories()
    return string.format("%d", math.floor(actualCalories))
end

-- ---------------------------------------------------------------------------------------- --
-- Pain Status
-- ---------------------------------------------------------------------------------------- --
ISPainStatusIndicator = MS_StatusIndicator:derive("ISPainStatusIndicator")

function ISPainStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Pained"
    o.__type = "ISPainStatusIndicator"
    return o
end

function ISPainStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Pain(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Bleeding Status
-- ---------------------------------------------------------------------------------------- --
ISBleedingStatusIndicator = MS_StatusIndicator:derive("ISBleedingStatusIndicator")

function ISBleedingStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Bleeding"
    o.__type = "ISBleedingStatusIndicator"
    return o
end

function ISBleedingStatusIndicator:getBleedingData()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.BleedingData(self.player)
    end
    return { level = 0, bleedingParts = 0, neckBleeding = false }
end

function ISBleedingStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Bleeding(self.player)
    end
    return 0
end

function ISBleedingStatusIndicator:getHoverText(value)
    local data = self:getBleedingData()
    return string.format("%d / 4", tonumber(data and data.level or value or 0) or 0)
end

-- ---------------------------------------------------------------------------------------- --
-- CarryWeight Status
-- ---------------------------------------------------------------------------------------- --
ISCarryWeightStatusIndicator = MS_StatusIndicator:derive("ISCarryWeightStatusIndicator")

function ISCarryWeightStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_HeavyLoad"
    o.__type = "ISCarryWeightStatusIndicator"
    return o
end

function ISCarryWeightStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.CarryWeight(self.player)
    end
    return 0
end

function ISCarryWeightStatusIndicator:getHoverText(value)
    local carryData = MS_PlayerStatus.Get.CarryWeightData(self.player)
    return string.format("%.1f / %.1f", carryData.weight or 0, carryData.capacity or 0)
end

-- ---------------------------------------------------------------------------------------- --
-- Proteins Status
-- ---------------------------------------------------------------------------------------- --
ISProteinsStatusIndicator = MS_StatusIndicator:derive("ISProteinsStatusIndicator")

function ISProteinsStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "proteins_icon"
    o.__type = "ISProteinsStatusIndicator"
    o.indicatorColor = {r=0.7, g=0.8, b=0.4}
    return o
end

function ISProteinsStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Proteins(self.player)
    end
    return 0
end

function ISProteinsStatusIndicator:getHoverText(value)
    local actualProteins = self.player:getNutrition():getProteins()
    return string.format("%.1f", actualProteins)
end

-- ---------------------------------------------------------------------------------------- --
-- Carbohydrates Status
-- ---------------------------------------------------------------------------------------- --
ISCarbohydratesStatusIndicator = MS_StatusIndicator:derive("ISCarbohydratesStatusIndicator")

function ISCarbohydratesStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "carbs_icon"
    o.__type = "ISCarbohydratesStatusIndicator"
    o.indicatorColor = {r=0.8, g=0.6, b=0.3}
    return o
end

function ISCarbohydratesStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Carbohydrates(self.player)
    end
    return 0
end

function ISCarbohydratesStatusIndicator:getHoverText(value)
    local actualCarbs = self.player:getNutrition():getCarbohydrates()
    return string.format("%.1f", actualCarbs)
end

-- ---------------------------------------------------------------------------------------- --
-- Lipids Status
-- ---------------------------------------------------------------------------------------- --
ISLipidsStatusIndicator = MS_StatusIndicator:derive("ISLipidsStatusIndicator")

function ISLipidsStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "lipids_icon"
    o.__type = "ISLipidsStatusIndicator"
    o.indicatorColor = {r=0.9, g=0.8, b=0.2}
    return o
end

function ISLipidsStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Lipids(self.player)
    end
    return 0
end

function ISLipidsStatusIndicator:getHoverText(value)
    local actualLipids = self.player:getNutrition():getLipids()
    return string.format("%.1f", actualLipids)
end


-- ---------------------------------------------------------------------------------------- --
-- Food Healing Status
-- ---------------------------------------------------------------------------------------- --
ISFoodHealingStatusIndicator = MS_StatusIndicator:derive("ISFoodHealingStatusIndicator")

function ISFoodHealingStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "FoodHealing"
    o.preferModernStatusColorIcon = true
    o.__type = "ISFoodHealingStatusIndicator"
    return o
end

local function MS_copyStableFoodHealingData(rawData, stableTimer)
    local data = {}
    if rawData then
        for key, value in pairs(rawData) do
            data[key] = value
        end
    end

    local timer = math.max(0, tonumber(stableTimer) or 0)
    local standard = tonumber(data.standard) or 1600
    local maxTimer = tonumber(data.maxTimer) or 11000

    data.timer = timer
    data.standard = standard
    data.maxTimer = maxTimer

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

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

    local gameTime = getGameTime and getGameTime() or nil
    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 = timer / (minutesPerDay * 2)
        end
    end

    return data
end

function ISFoodHealingStatusIndicator:getFoodHealingData()
    if not self.player or self.player:isDead() then
        return nil
    end

    local now = getTimestampMs and getTimestampMs() or 0
    if self._foodHealingData and (now - (self._foodHealingDataTime or 0)) < 250 then
        return self._foodHealingData
    end

    local rawData = MS_PlayerStatus.Get.FoodHealingData(self.player)
    local rawTimer = math.max(0, tonumber(rawData and rawData.timer) or 0)
    local stableTimer = rawTimer

    local lastTimer = tonumber(self._foodHealingDisplayTimer)
    local lastTime = tonumber(self._foodHealingDisplayTime)
    if lastTimer and lastTime and now > lastTime then
        local elapsedSeconds = math.max(0, (now - lastTime) / 1000)
        local predictedTimer = math.max(0, lastTimer - (elapsedSeconds * 48))

        if rawTimer <= 0 then
            stableTimer = 0
        elseif rawTimer > predictedTimer + 500 then
            -- Treat larger upward jumps as real new food intake, not MP sync noise.
            stableTimer = rawTimer
        elseif rawTimer < predictedTimer - 240 then
            -- Large downward corrections should be accepted so the UI can catch up.
            stableTimer = rawTimer
        else
            -- Ignore small upward/downward MP Host sync oscillations and display a monotonic timer.
            stableTimer = predictedTimer
        end
    end

    self._foodHealingDisplayTimer = stableTimer
    self._foodHealingDisplayTime = now

    local data = MS_copyStableFoodHealingData(rawData, stableTimer)
    self._foodHealingData = data
    self._foodHealingDataTime = now
    return data
end

function ISFoodHealingStatusIndicator:getValue()
    local data = self:getFoodHealingData()
    return data and data.normalized or 0
end

function ISFoodHealingStatusIndicator:getHoverText(value)
    local data = self:getFoodHealingData()
    local seconds = math.max(0, math.floor((tonumber(data and data.irlSeconds) or 0) + 0.5))
    local minutes = math.floor(seconds / 60)
    local rem = seconds % 60
    if minutes > 0 then
        return string.format("%dm %02ds", minutes, rem)
    end
    return string.format("%ds", rem)
end

-- ---------------------------------------------------------------------------------------- --
-- Weight Status
-- ---------------------------------------------------------------------------------------- --
ISWeightStatusIndicator = MS_StatusIndicator:derive("ISWeightStatusIndicator")

function ISWeightStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "weight_icon"
    o.__type = "ISWeightStatusIndicator"
    o.indicatorColor = {r=0.5, g=0.6, b=0.7}
    o.useGradient = true
    return o
end

function ISWeightStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Weight(self.player)
    end
    return 0.5
end

function ISWeightStatusIndicator:getGradientColor(value)
    if not self.player then return {r=1, g=1, b=1} end
    
    local weight = self.player:getNutrition():getWeight()
    
    -- too heavy
    if weight > 100 then
        local factor = math.min((weight - 100) / 30, 1.0)
        return {
            r = 1.0,
            g = 0.5 - factor * 0.5,
            b = 0.0
        }
    -- a bit high
    elseif weight > 85 then
        local factor = (weight - 85) / 15
        return {
            r = 0.5 + factor * 0.5,
            g = 1.0 - factor * 0.5,
            b = 0.0
        }
    -- healty weight
    elseif weight > 75 then
        return {r=0.0, g=1.0, b=0.0}
    -- a bit low
    elseif weight > 50 then
        local factor = (weight - 50) / 25
        return {
            r = 0.0,
            g = factor,
            b = 1.0 - factor
        }
    -- too low
    else
        local factor = math.max((weight - 35) / 15, 0.0)
        return {
            r = 0.0,
            g = 0.0 + factor * 0.5,
            b = 0.5 + factor * 0.5
        }
    end
end

function ISWeightStatusIndicator:getHoverText(value)
    local actualWeight = self.player:getNutrition():getWeight()
    return string.format("%.1f", actualWeight)
end

-- ---------------------------------------------------------------------------------------- --
-- Dirtiness Status
-- ---------------------------------------------------------------------------------------- --
ISDirtinessStatusIndicator = MS_StatusIndicator:derive("ISDirtinessStatusIndicator")

function ISDirtinessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Dirty"
    o.__type = "ISDirtinessStatusIndicator"
    o.indicatorColor = {r=0.5, g=0.4, b=0.3}
    return o
end

function ISDirtinessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Dirtiness(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Bloodiness Status
-- ---------------------------------------------------------------------------------------- --
ISBloodinessStatusIndicator = MS_StatusIndicator:derive("ISBloodinessStatusIndicator")

function ISBloodinessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Bloodiness"
    o.preferModernStatusColorIcon = true
    o.__type = "ISBloodinessStatusIndicator"
    return o
end

function ISBloodinessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Bloodiness(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Anger Status
-- ---------------------------------------------------------------------------------------- --
ISAngerStatusIndicator = MS_StatusIndicator:derive("ISAngerStatusIndicator")

function ISAngerStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Angry"
    o.__type = "ISAngerStatusIndicator"
    o.indicatorColor = {r=1, g=0.1, b=0.1}
    return o
end

function ISAngerStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Anger(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Wetness Status
-- ---------------------------------------------------------------------------------------- --
ISWetnessStatusIndicator = MS_StatusIndicator:derive("ISWetnessStatusIndicator")

function ISWetnessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Wet"
    o.__type = "ISWetnessStatusIndicator"
    o.indicatorColor = {r=0.4, g=0.6, b=0.9}
    return o
end

function ISWetnessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Wetness(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Drunkenness Status
-- ---------------------------------------------------------------------------------------- --
ISDrunkennessStatusIndicator = MS_StatusIndicator:derive("ISDrunkennessStatusIndicator")

function ISDrunkennessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_Drunk"
    o.__type = "ISDrunkennessStatusIndicator"
    o.indicatorColor = {r=0.8, g=0.4, b=0.6}
    return o
end

function ISDrunkennessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Drunkenness(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Stiffness Status
-- ---------------------------------------------------------------------------------------- --
ISStiffnessStatusIndicator = MS_StatusIndicator:derive("ISStiffnessStatusIndicator")

function ISStiffnessStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Status_Stiffness"
    o.__type = "ISStiffnessStatusIndicator"
    o.indicatorColor = {r=0.8, g=0.6, b=0.0}
    return o
end

function ISStiffnessStatusIndicator:getValue()
    if self.player and not self.player:isDead() then
        return MS_PlayerStatus.Get.Stiffness(self.player)
    end
    return 0
end

-- ---------------------------------------------------------------------------------------- --
-- Noxious Exposure Status
-- ---------------------------------------------------------------------------------------- --
ISNoxiousExposureStatusIndicator = MS_StatusIndicator:derive("ISNoxiousExposureStatusIndicator")

function ISNoxiousExposureStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Mood_NoxiousSmell"
    o.__type = "ISNoxiousExposureStatusIndicator"
    o.indicatorColor = {r=0.79, g=0.71, b=0.35}
    o.useGradient = true
    return o
end

function ISNoxiousExposureStatusIndicator:getExposureData()
    local currentTime = getTimestampMs and getTimestampMs() or 0
    local interval = self.getUpdateIntervalMs and self:getUpdateIntervalMs() or 1000
    local cache = self._noxiousExposureCache
    if cache and (currentTime - (cache.time or 0)) < interval then
        return cache.data
    end

    local data
    if not self.player or self.player:isDead() then
        data = MS_NoxiousExposure.getData(nil)
    else
        data = MS_NoxiousExposure.getData(self.player)
    end

    self._noxiousExposureCache = { time = currentTime, data = data }
    return data
end

function ISNoxiousExposureStatusIndicator:getValue()
    if not self.player or self.player:isDead() then
        return 0
    end

    local data = self:getExposureData()
    return tonumber(data and data.normalizedValue or 0) or 0
end

function ISNoxiousExposureStatusIndicator:getHoverText(value)
    local data = self:getExposureData()
    return string.format("%.1f", tonumber(data and data.displayValue or 0) or 0)
end

-- ---------------------------------------------------------------------------------------- --
-- Medicine Status
-- ---------------------------------------------------------------------------------------- --
ISMedicineStatusIndicator = MS_StatusIndicator:derive("ISMedicineStatusIndicator")

ISMedicineStatusIndicator.MEDICINE_TYPES = {
    { name = "Painkillers", getText = "IGUI_ModernStatus_Painkillers", iconName = "Painkillers", maxEffect = 5400, getRawEffect = function(player) return player:getPainEffect() end, getEffect = function(player) return player:getPainEffect() / 5400 end },
    { name = "Antibiotics", getText = "IGUI_ModernStatus_Antibiotics", iconName = "Antibiotics", maxEffect = 50, getRawEffect = function(player) return player:getReduceInfectionPower() end, getEffect = function(player) return player:getReduceInfectionPower() / 50 end },
    { name = "BetaBlockers", getText = "IGUI_ModernStatus_BetaBlockers", iconName = "BetaBlockers", maxEffect = 6600, getRawEffect = function(player) return player:getBetaEffect() end, getEffect = function(player) return player:getBetaEffect() / 6600 end },
    { name = "Antidepressants", getText = "IGUI_ModernStatus_Antidepressants", iconName = "Antidepressants", maxEffect = 6600, getRawEffect = function(player) return player:getDepressEffect() end, getEffect = function(player) return player:getDepressEffect() / 6600 end },
    { name = "SleepingTablets", getText = "IGUI_ModernStatus_SleepingTablets", iconName = "SleepingTablets", maxEffect = 6600, getRawEffect = function(player) return player:getSleepingTabletEffect() end, getEffect = function(player) return player:getSleepingTabletEffect() / 6600 end }
}

function ISMedicineStatusIndicator:new(x, y, width, height, player)
    local o = MS_StatusIndicator.new(self, x, y, width, height, player)
    o.baseIconName = "Painkillers"
    o.__type = "ISMedicineStatusIndicator"
    o.indicatorColor = {r=0.3, g=0.9, b=0.4}
    o.displayIndex = 1 -- Start from painkiller
    o.switchTimer = 0
    o.switchInterval = 20 -- time to change 
    return o
end

function ISMedicineStatusIndicator:update()
    self.switchTimer = self.switchTimer + (1/30)

    if self.switchTimer >= self.switchInterval then
        self.switchTimer = 0

        local hasAnyEffect = false
        local startIndex = self.displayIndex
        
        repeat
            self.displayIndex = self.displayIndex + 1
            if self.displayIndex > #self.MEDICINE_TYPES then
                self.displayIndex = 1
            end

            local effect = self.MEDICINE_TYPES[self.displayIndex].getEffect(self.player)
            if effect > 0 then
                hasAnyEffect = true
                break
            end
        until self.displayIndex == startIndex
        if not hasAnyEffect then
            self.displayIndex = 1
        end

        self:updateMedicineIcon()
    end
end

function ISMedicineStatusIndicator:updateMedicineIcon()
    local currentMedicine = self.MEDICINE_TYPES[self.displayIndex]
    self.baseIconName = currentMedicine.iconName
    self:updateIconPath()
end

function ISMedicineStatusIndicator:getValue()
    if not self.player or self.player:isDead() then return 0 end
    
    self:update()
    
    local currentMedicine = self.MEDICINE_TYPES[self.displayIndex]
    return currentMedicine.getEffect(self.player)
end

function ISMedicineStatusIndicator:getCurrentMedicineEffectSeconds()
    if not self.player or self.player:isDead() then return 0 end

    -- Return the raw remaining effect units for threshold checks. Most vanilla
    -- medicine effects are timer-like values, while antibiotics use their
    -- infection-reduction power directly.
    local currentMedicine = self.MEDICINE_TYPES[self.displayIndex]
    if currentMedicine and currentMedicine.getRawEffect then
        return currentMedicine.getRawEffect(self.player) or 0
    end

    if currentMedicine and currentMedicine.getEffect then
        return (currentMedicine.getEffect(self.player) or 0) * (currentMedicine.maxEffect or 1)
    end

    return 0
end

return MS_StatusIndicator
