code:
using RimWorld;
using RimWorld.Planet;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Verse;

namespace RJW_Menstruation
{
    [Flags]
    public enum SeasonalBreed
    {
        Always = 0,
        Spring = 1,
        Summer = 2,
        Fall = 4,
        Winter = 8,
        FirstHalf = Spring | Summer,
        SecondHalf = Fall | Winter
    }

    public class CompProperties_Menstruation : HediffCompProperties
    {
        public float maxCumCapacity; // ml
        public float baseImplantationChanceFactor;
        public float basefertilizationChanceFactor;
        public int follicularIntervalDays = 14; //before ovulation including beginning of bleeding
        public int lutealIntervalDays = 14; //after ovulation until bleeding
        public int bleedingIntervalDays = 6; //must be less than folicularIntervalDays
        public int recoveryIntervalDays = 10; //additional infertile days after gave birth
        public int eggLifespanDays = 2; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday
        public string wombTex = "Womb/Womb"; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday
        public string vagTex = "Genitals/Vagina"; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday
        public bool infertile = false;
        public bool concealedEstrus = false;
        public SeasonalBreed breedingSeason = SeasonalBreed.Always;
        public int estrusDaysBeforeOvulation = 3;
        public int eggMultiplier = 1;

        public CompProperties_Menstruation()
        {

            compClass = typeof(HediffComp_Menstruation);
        }
    }

    public class CompProperties_Anus : HediffCompProperties
    {
        public string analTex = "Genitals/Anal";

        public CompProperties_Anus()
        {
            compClass = typeof(HediffComp_Anus);
        }
    }

    public class HediffComp_Menstruation : HediffComp
    {
        const float minmakefilthvalue = 1.0f;
        //const int ovarypowerthreshold = 72;

        const int tickInterval = GenDate.TicksPerHour;
        const int maxImplantDelayHours = 30 * 24;
        const int minImplantAgeHours = 3 * 24;

        public CompProperties_Menstruation Props;
        public Stage curStage = Stage.Follicular;
        public int curStageHrs = 0;
        public bool loaded = false;
        public bool initError = false;
        public int ovarypower = -100000;
        public int eggstack = 0;
        public bool DoCleanWomb = false;

        public enum Stage
        {
            Follicular,
            Ovulatory,
            Luteal,
            Bleeding,
            Pregnant,
            Recover,
            None,
            Young,
            ClimactericFollicular,  // obsolete
            ClimactericLuteal,      // obsolete
            ClimactericBleeding,    // obsolete
            Anestrus
        }

        public enum EstrusLevel
        {
            None,
            Concealed,
            Visible
        }

        public static readonly Dictionary<Stage, Texture2D> StageTexture = new Dictionary<Stage, Texture2D>()
        {
            { Stage.Follicular, TextureCache.FollicularTexture },
            { Stage.ClimactericFollicular, TextureCache.FollicularTexture },
            { Stage.Luteal, TextureCache.LutealTexture },
            { Stage.ClimactericLuteal, TextureCache.LutealTexture },
            { Stage.Bleeding, TextureCache.BleedingTexture },
            { Stage.ClimactericBleeding, TextureCache.BleedingTexture },
            { Stage.Pregnant, TextureCache.PregnantTexture },
            { Stage.Recover, TextureCache.RecoverTexture }
        };

        protected List<Cum> cums;
        protected List<Egg> eggs;
        protected float cycleSpeed = -1;
        protected float cycleVariability = -1;
        protected int currentIntervalHours = -1;
        protected float crampPain = -1;
        protected Need sexNeed = null;
        protected string customwombtex = null;
        protected string customvagtex = null;
        protected bool estrusflag = false;
        protected int opcache = -1;
        protected float antisperm = 0.0f;
        protected float? originvagsize = null;
        protected Hediff_BasePregnancy pregnancy = null;

        private static readonly SimpleCurve SexFrequencyCurve = new SimpleCurve()
        {
            new CurvePoint(0.4f,0.05f),
            new CurvePoint(0.6f,0.1f),
            new CurvePoint(0.8f,0.25f),
            new CurvePoint(1.0f,0.5f)
        };

        private static readonly SimpleCurve SexSatisfactionCurve = new SimpleCurve()
        {
            new CurvePoint(0.4f,0.5f),
            new CurvePoint(0.6f,0.6f),
            new CurvePoint(0.8f,0.7f),
            new CurvePoint(1.0f,0.8f)
        };

        private static readonly SimpleCurve FertilityCurve = new SimpleCurve()
        {
            new CurvePoint(0.4f,0.01f),
            new CurvePoint(0.6f,0.1f),
            new CurvePoint(0.8f,0.25f),
            new CurvePoint(1.0f,0.5f)
        };

        public Hediff_BasePregnancy Pregnancy {
            get
            {
                if (pregnancy == null) return null;
                else if (!Pawn.health.hediffSet.hediffs.Contains(pregnancy))
                {
                    pregnancy = null;
                    return null;
                }
                else return pregnancy;
            }
            set => pregnancy = value;
        }

        public int OvaryPowerThreshold
        {
            get
            {
                if (opcache >= 0) return opcache;
                float avglittersize;
                try
                {
                    avglittersize = Mathf.Max(Rand.ByCurveAverage(Pawn.def.race.litterSizeCurve), 1.0f);
                }
                catch
                {
                    // Any exceptions in that will have been reported elsewhere in the code by now
                    avglittersize = 1.0f;
                };
                const float yearsBeforeMenopause = 6.0f;
                opcache = (int)(RaceCyclesPerYear() *
                    avglittersize *
                    yearsBeforeMenopause *
                    (Pawn.def.race.lifeExpectancy / ThingDefOf.Human.race.lifeExpectancy));
                return opcache;
            }
        }

        // >= 1: Normal cycles
        // 1 - 0: Climacteric
        // <= 0: Menopause
        public float EggHealth
        {
            get
            {
                if (!Configurations.EnableMenopause || Props.infertile) return Mathf.Min(1.0f, ovarypower / OvaryPowerThreshold);
                else return ovarypower / OvaryPowerThreshold;
            }
        }

        public float SexFrequencyModifier
        {
            get
            {
                float eggHealth = EggHealth;
                if (eggHealth >= 1) return 1.0f;
                else if (eggHealth <= 0) return 0.01f;
                else return SexFrequencyCurve.Evaluate(eggHealth);
            }
        }

        public float SexSatisfactionModifier
        {
            get
            {
                float eggHealth = EggHealth;
                if (eggHealth >= 1) return 1.0f;
                else if (eggHealth <= 0) return 0.5f;
                else return SexSatisfactionCurve.Evaluate(eggHealth);
            }
        }

        public float FertilityModifier
        {
            get
            {
                float eggHealth = EggHealth;
                if (eggHealth >= 1) return 1.0f;
                else if (eggHealth <= 0) return 0.0f;
                else return FertilityCurve.Evaluate(eggHealth);
            }
        }

        public float TotalCum
        {
            get
            {
                return cums?.Sum(cum => cum.Volume) ?? 0;
            }
        }
        public float TotalFertCum
        {
            get
            {
                return cums?.Sum(cum => cum.FertVolume) ?? 0;
            }
        }
        public float TotalCumPercent
        {
            get
            {
                return cums?.Sum(cum => cum.Volume) / Props.maxCumCapacity ?? 0;
            }
        }
        public float CumCapacity
        {
            get
            {
                float res = Props.maxCumCapacity * Pawn.BodySize;
                if (curStage != Stage.Pregnant || (pregnancy?.Severity ?? 0f) < 0.175f) res *= 500f;
                return res;
            }
        }
        //make follicular interval into half and double egg lifespan
        public float CycleFactor
        {
            get
            {
                if (xxx.has_quirk(Pawn, "Breeder")) return 0.5f;

                return 1.0f;
            }
        }
        //effect on implant chance
        public float ImplantFactor
        {
            get
            {
                float factor = 1.0f;
                if (Pawn.Has(Quirk.Breeder)) factor = 10.0f;
                return Pawn.health.capacities.GetLevel(xxx.reproduction) * Props.baseImplantationChanceFactor * FertilityModifier * factor;
            }
        }
        public IEnumerable<string> GetCumsInfo
        {
            get
            {
                if (cums.NullOrEmpty()) yield return Translations.Info_noCum;
                else foreach (Cum cum in cums)
                    {
                        if (!cum.notcum) yield return string.Format("{0}: {1:0.##}ml", cum.pawn?.Label, cum.Volume);
                        else yield return string.Format("{0}: {1:0.##}ml", cum.notcumLabel, cum.Volume);
                    }
            }
        }
        public Color GetCumMixtureColor
        {
            get
            {
                Color mixedcolor = Color.white;

                if (cums.NullOrEmpty()) return mixedcolor;

                float mixedsofar = 0;
                foreach (Cum cum in cums)
                {
                    if (cum.Volume > 0)
                    {
                        mixedcolor = Colors.CMYKLerp(mixedcolor, cum.Color, cum.Volume / (mixedsofar + cum.Volume));
                        mixedsofar += cum.Volume;
                    }
                }
                return mixedcolor;
            }
        }

        public Stage CurrentVisibleStage
        {
            get
            {
                if (curStage == Stage.Pregnant)
                {
                    if (Configurations.InfoDetail == Configurations.DetailLevel.All || (pregnancy?.Visible ?? false))
                        return Stage.Pregnant;
                    else
                        return Stage.Luteal;
                }
                return curStage;
            }
        }

        public string GetCurStageLabel
        {
            get
            {
                switch (CurrentVisibleStage)
                {
                    case Stage.Follicular:
                        return Translations.Stage_Follicular + (EggHealth < 1f ? Translations.Stage_Climacteric : "");
                    case Stage.Ovulatory:
                        return Translations.Stage_Ovulatory + (EggHealth < 1f ? Translations.Stage_Climacteric : "");
                    case Stage.Luteal:
                        return Translations.Stage_Luteal + (EggHealth < 1f ? Translations.Stage_Climacteric : "");
                    case Stage.Bleeding:
                        return Translations.Stage_Bleeding + (EggHealth < 1f ? Translations.Stage_Climacteric : "");
                    case Stage.Pregnant:
                        return Translations.Stage_Pregnant;
                    case Stage.Recover:
                        return Translations.Stage_Recover;
                    case Stage.None:
                    case Stage.Young:
                        if (EggHealth <= 0f) return Translations.Stage_Menopause;
                        else return Translations.Stage_None;
                    case Stage.ClimactericFollicular:
                        return Translations.Stage_Follicular + " - " + Translations.Stage_Climacteric;
                    case Stage.ClimactericLuteal:
                        return Translations.Stage_Luteal + " - " + Translations.Stage_Climacteric;
                    case Stage.ClimactericBleeding:
                        return Translations.Stage_Bleeding + " - " + Translations.Stage_Climacteric;
                    case Stage.Anestrus:
                        return Translations.Stage_Anestrus;
                    default:
                        return "";
                }
            }

        }
        public virtual string GetCurStageDesc
        {
            get
            {
                switch (CurrentVisibleStage)
                {
                    case Stage.Follicular:
                        return Translations.Stage_Follicular_Desc + (EggHealth < 1f ? Translations.Stage_Climacteric_Desc : "");
                    case Stage.Ovulatory:
                        return Translations.Stage_Ovulatory_Desc + (EggHealth < 1f ? Translations.Stage_Climacteric_Desc : "");
                    case Stage.Luteal:
                        return Translations.Stage_Luteal_Desc + (EggHealth < 1f ? Translations.Stage_Climacteric_Desc : "");
                    case Stage.Bleeding:
                        return Translations.Stage_Bleeding_Desc + (EggHealth < 1f ? Translations.Stage_Climacteric_Desc : "");
                    case Stage.Pregnant:
                        return Translations.Stage_Pregnant_Desc;
                    case Stage.Recover:
                        return Translations.Stage_Recover_Desc;
                    case Stage.None:
                    case Stage.Young:
                        if (EggHealth <= 0f) return Translations.Stage_Menopause_Desc;
                        else return Translations.Stage_None_Desc;
                    case Stage.ClimactericFollicular:
                        return Translations.Stage_Follicular_Desc + " " + Translations.Stage_Climacteric_Desc;
                    case Stage.ClimactericLuteal:
                        return Translations.Stage_Luteal_Desc + " " + Translations.Stage_Climacteric_Desc;
                    case Stage.ClimactericBleeding:
                        return Translations.Stage_Bleeding_Desc + " " + Translations.Stage_Climacteric_Desc;
                    case Stage.Anestrus:
                        return Translations.Stage_Anestrus_Desc;
                    default:
                        return "";
                }
            }
        }

        public string WombTex
        {
            get
            {
                return customwombtex ?? Props.wombTex;
            }
            set
            {
                customwombtex = value;
            }
        }
        public string VagTex
        {
            get
            {
                return customvagtex ?? Props.vagTex;
            }
            set
            {
                customvagtex = value;
            }
        }
        public string GetFertilizingInfo
        {
            get
            {
                if (eggs.NullOrEmpty()) return "";

                StringBuilder res = new StringBuilder();
                int fertilized = eggs.Count(egg => egg.fertilized);
                if (fertilized != 0) res.AppendFormat("{0} {1}", fertilized, Translations.Dialog_WombInfo05);
                if (fertilized != 0 && eggs.Count - fertilized != 0) res.Append(", ");
                if (cums.NullOrEmpty() || TotalFertCum == 0)
                {
                    if (eggs.Count - fertilized != 0) res.AppendFormat("{0} {1}", eggs.Count - fertilized, Translations.Dialog_WombInfo07);
                }
                else
                {
                    if (eggs.Count - fertilized != 0) res.AppendFormat("{0} {1}", eggs.Count - fertilized, Translations.Dialog_WombInfo06);
                }
                return res.ToString();
            }
        }
        public bool IsEggFertilizing
        {
            get
            {
                if (eggs.NullOrEmpty()) return false;
                return cums?.Any(cum => cum.FertVolume > 0) ?? false;
            }
        }
        /// <summary>
        /// returns fertstage. if not fertilized returns -1
        /// </summary>
        public int IsFertilized
        {
            get
            {
                if (eggs?.All(egg => !egg.fertilized) ?? true) return -1;
                return eggs.Max(egg => egg.fertstage);
            }
        }
        public IEnumerable<Pawn> GetCummersAndFertilizers()
        {
            if (!cums.NullOrEmpty())
                foreach (Cum cum in cums)
                    yield return cum.pawn;
            if (!eggs.NullOrEmpty())
                foreach (Egg egg in eggs)
                    yield return egg.fertilizer;
        }
        public bool IsEggExist
        {
            get
            {
                return !eggs.NullOrEmpty();
            }
        }
        public virtual bool IsDangerDay
        {
            get
            {
                if (Pawn.health.hediffSet.HasHediff(VariousDefOf.RJW_IUD)) return false;

                switch (curStage)
                {
                    case Stage.Follicular:
                    case Stage.ClimactericFollicular:
                        return curStageHrs > 0.7f * currentIntervalHours;
                    case Stage.Ovulatory:
                        return true;
                    case Stage.Luteal:
                    case Stage.ClimactericLuteal:
                        return curStageHrs < Props.eggLifespanDays * 24;
                    default:
                        return false;
                }
            }
        }
        public int GetNumOfEggs
        {
            get
            {
                return eggs?.Count ?? 0;
            }
        }
        public Color BloodColor
        {
            get
            {
                try
                {
                    Color c = Pawn.def.race.BloodDef.graphicData.color;
                    return c;
                }
                catch
                {
                    return Colors.blood;
                }

            }
        }

        public float OriginVagSize
        {
            get
            {
                if (originvagsize == null)
                {
                    originvagsize = parent.Severity;
                }
                return originvagsize ?? 0.1f;
            }
            set
            {
                originvagsize = value;
            }
        }

        public float CurStageIntervalHours
        {
            get
            {
                return currentIntervalHours;
            }
        }

        public float StageProgress
        {
            get
            {
                if (pregnancy == null) return Mathf.Clamp01(curStageHrs / CurStageIntervalHours);
                if (pregnancy.is_discovered || Configurations.infoDetail == Configurations.DetailLevel.All) return pregnancy.Severity;

                // Luteal will appear to progress, hitting the end of the phase when the pregnancy is discovered
                float discoveryTime = 0.5f;
                if (Pawn.story?.bodyType == BodyTypeDefOf.Thin) discoveryTime = 0.25f;
                else if (Pawn.story?.bodyType == BodyTypeDefOf.Female) discoveryTime = 0.35f;
                // Estimated; there's no way to get the exact value after the fact without writing it into the save
                float lutealProgressWhenImplanted = Math.Min(0.5f, maxImplantDelayHours / (Props.lutealIntervalDays * 24));

                return GenMath.LerpDouble(0, discoveryTime, lutealProgressWhenImplanted, 1.0f, pregnancy.Severity);
            }
        }

        public Texture2D GetStageTexture
        {
            get
            {
                if (!StageTexture.TryGetValue(CurrentVisibleStage, out Texture2D tex)) tex = TextureCache.PregnantTexture;
                return tex;
            }
        }

        public override void CompExposeData()
        {
            base.CompExposeData();
            Scribe_Collections.Look(ref cums, saveDestroyedThings: true, label: "cums", lookMode: LookMode.Deep, ctorArgs: new object[0]);
            Scribe_Collections.Look(ref eggs, saveDestroyedThings: true, label: "eggs", lookMode: LookMode.Deep, ctorArgs: new object[0]);
            Scribe_Values.Look(ref curStage, "curStage", curStage, true);
            Scribe_Values.Look(ref curStageHrs, "curStageHrs", curStageHrs, true);
            Scribe_Values.Look(ref cycleSpeed, "cycleSpeed", cycleSpeed, true);
            Scribe_Values.Look(ref cycleVariability, "cycleVariability", cycleVariability, true);
            Scribe_Values.Look(ref currentIntervalHours, "currentIntervalHours", currentIntervalHours, true);
            Scribe_Values.Look(ref crampPain, "crampPain", crampPain, true);
            Scribe_Values.Look(ref ovarypower, "ovarypower", ovarypower, true);
            Scribe_Values.Look(ref eggstack, "eggstack", eggstack, true);
            Scribe_Values.Look(ref estrusflag, "estrusflag", estrusflag, true);
            Scribe_Values.Look(ref originvagsize, "originvagsize", originvagsize, true);
            Scribe_Values.Look(ref DoCleanWomb, "DoCleanWomb", DoCleanWomb, true);
            Scribe_References.Look(ref pregnancy, "pregnancy");
        }

        public override void CompPostPostAdd(DamageInfo? dinfo)
        {
            if (!loaded)
            {
                Initialize();
            }
        }

        public bool ShouldSimulate()
        {
            if (!Configurations.EnableAnimalCycle && Pawn.IsAnimal()) return false;
            if (Pawn.Spawned || Pawn.IsCaravanMember() || PawnUtility.IsTravelingInTransportPodWorldObject(Pawn)) return true;

            return false;
        }

        public override void CompPostTick(ref float severityAdjustment)
        {
            base.CompPostTick(ref severityAdjustment);
            // If an exception makes it out, RW will remove the hediff, so catch it here
            try
            {
                if (!ShouldSimulate()) return;

                // Initialize immediately if needed, but if there's an error, then don't spam it every tick
                if (!loaded && !initError)
                {
                    Log.Warning($"{Pawn}'s womb is ticking, but was not initialized first");
                    Initialize();
                }

                if (!Pawn.IsHashIntervalTick(tickInterval)) return;

                if (initError) Log.Warning($"Attempting to process {Pawn}'s womb uninitialized");

                if (Pregnancy != null && curStage != Stage.Pregnant)
                {
                    Log.Warning($"{Pawn}'s womb has a pregnancy, but was not in the pregnant stage");
                    curStage = Stage.Pregnant;
                }

                CumOut();

                if (curStage == Stage.ClimactericFollicular) curStage = Stage.Follicular;
                else if (curStage == Stage.ClimactericLuteal) curStage = Stage.Luteal;
                else if (curStage == Stage.ClimactericBleeding) curStage = Stage.Bleeding;

                if (pregnancy == null && (Pawn.health.capacities.GetLevel(xxx.reproduction) <= 0) || EggHealth <= 0) curStage = Stage.Young;
                switch (curStage)
                {
                    case Stage.Follicular:
                        FollicularAction();
                        break;
                    case Stage.Ovulatory:
                        OvulatoryAction();
                        break;
                    case Stage.Luteal:
                        LutealAction();
                        break;
                    case Stage.Bleeding:
                        BleedingAction();
                        break;
                    case Stage.Pregnant:
                        PregnantAction();
                        break;
                    case Stage.Recover:
                        RecoverAction();
                        break;
                    case Stage.None:
                        break;
                    case Stage.Young:
                        YoungAction();
                        break;
                    case Stage.Anestrus:
                        AnestrusAction();
                        break;
                    default:
                        GoNextStage(Stage.Follicular);
                        break;
                }
                AfterSimulator();
            }
            catch (Exception ex)
            {
                Log.Error($"Error processing womb of {Pawn}: {ex}");
            }
        }

        public override void CompPostPostRemoved()
        {
            // If a hediff is removed from a pawn that does not have it, CompPostPostRemoved is still called on the pawn that does.
            // If it was a legitimate removal, then it won't be in this pawn's hediff list anymore, as that removal occurs first
            if (Pawn.health.hediffSet.hediffs.Contains(parent))
            {
                Log.Warning($"Attempted to remove menstruation comp from wrong pawn ({Pawn}).");
                return;
            }
            if (pregnancy != null && !(pregnancy is Hediff_MechanoidPregnancy))
                pregnancy.Miscarry();
            base.CompPostPostRemoved();
        }

        /// <summary>
        /// Get fluid in womb that not a cum
        /// </summary>
        /// <param name="notcumlabel"></param>
        /// <returns></returns>
        public Cum GetNotCum(string notcumlabel)
        {
            if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
                {
                    if (cum.notcum && cum.notcumLabel.Equals(notcumlabel)) return cum;
                }
            return null;
        }

        /// <summary>
        /// Get pawn's cum in womb
        /// </summary>
        /// <param name="pawn"></param>
        /// <returns></returns>
        public Cum GetCum(Pawn pawn)
        {
            return cums?.Find(cum => !cum.notcum && cum.pawn == pawn);
        }

        /// <summary>
        /// Inject pawn's cum into womb
        /// </summary>
        /// <param name="pawn"></param>
        /// <param name="volume"></param>
        /// <param name="fertility"></param>
        /// <param name="filthdef"></param>
        public void CumIn(Pawn pawn, float volume, float fertility = 1.0f, ThingDef filthdef = null)
        {
            if (volume <= 0) return;
            if (Pawn.health.hediffSet.HasHediff(VariousDefOf.RJW_IUD)) fertility /= 100f;
            float cumd = TotalCumPercent;
            float tmp = TotalCum + volume;
            if (tmp > CumCapacity)
            {
                float cumoutrate = 1 - (CumCapacity / tmp);
                bool merged = false;
                if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
                    {
                        if (cum.pawn.Equals(pawn))
                        {
                            cum.MergeWithCum(volume, fertility, filthdef);
                            merged = true;
                        }
                        cum.DismishForce(cumoutrate);
                    }
                if (!merged) cums.Add(new Cum(pawn, volume * (1 - cumoutrate), fertility, filthdef));
            }
            else
            {

                bool merged = false;
                if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
                    {
                        if (cum.pawn.Equals(pawn))
                        {
                            cum.MergeWithCum(volume, fertility, filthdef);
                            merged = true;
                        }
                    }
                if (!merged) cums.Add(new Cum(pawn, volume, fertility, filthdef));
            }
            cumd = TotalCumPercent - cumd;

            Pawn.records.AddTo(VariousDefOf.AmountofCreampied, volume);
            AfterCumIn(pawn);
            AfterFluidIn(cumd);
        }

        /// <summary>
        /// Inject pawn's fluid into womb
        /// </summary>
        /// <param name="pawn"></param>
        /// <param name="volume"></param>
        /// <param name="notcumlabel"></param>
        /// <param name="decayresist"></param>
        /// <param name="filthdef"></param>
        public void CumIn(Pawn pawn, float volume, string notcumlabel, float decayresist = 0, ThingDef filthdef = null)
        {
            if (volume <= 0) return;
            float tmp = TotalCum + volume;
            float cumd = TotalCumPercent;
            if (tmp > CumCapacity)
            {
                float cumoutrate = 1 - (CumCapacity / tmp);
                bool merged = false;
                if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
                    {
                        if (cum.notcum && cum.pawn.Equals(pawn) && cum.notcumLabel.Equals(notcumlabel))
                        {
                            cum.MergeWithFluid(volume, decayresist, filthdef);
                            merged = true;
                        }
                        cum.DismishForce(cumoutrate);
                    }
                if (!merged) cums.Add(new Cum(pawn, volume * (1 - cumoutrate), notcumlabel, decayresist, filthdef));
            }
            else
            {

                bool merged = false;
                if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
                    {
                        if (cum.notcum && cum.pawn.Equals(pawn) && cum.notcumLabel.Equals(notcumlabel))
                        {
                            cum.MergeWithFluid(volume, decayresist, filthdef);
                            merged = true;
                        }
                    }
                if (!merged) cums.Add(new Cum(pawn, volume, notcumlabel, decayresist, filthdef));
            }
            cumd = TotalCumPercent - cumd;
            AfterNotCumIn();
            AfterFluidIn(cumd);
        }

        protected virtual void AfterCumIn(Pawn cummer)
        {
            ThoughtCumInside(cummer);
            TaleCumInside(cummer);
        }

        protected virtual void AfterNotCumIn()
        {

        }

        /// <summary>
        /// Action for both Cum and NotCum
        /// </summary>
        /// <param name="fd">Fluid deviation</param>
        protected virtual void AfterFluidIn(float fd)
        {

        }

        protected void BeforeCumOut(out Absorber absorber)
        {
            Hediff asa = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_ASA);
            float asafactor = asa?.Severity ?? 0f;

            if (Pawn.health.hediffSet.HasHediff(VariousDefOf.RJW_IUD)) antisperm = 0.70f + asafactor;
            else antisperm = 0.0f + asafactor;

            absorber = (Absorber)Pawn.apparel?.WornApparel?.Find(x => x is Absorber);
            if (absorber != null)
            {
                absorber.WearEffect();
                if (absorber.dirty && absorber.EffectAfterDirty) absorber.DirtyEffect();
            }
        }

        /// <summary>
        /// For natural leaking
        /// </summary>
        protected virtual void AfterCumOut()
        {
            Pawn.needs?.mood?.thoughts?.memories?.TryGainMemory(VariousDefOf.LeakingFluids);
        }

        /// <summary>
        /// For all type of leaking
        /// </summary>
        /// <param name="fd"></param>
        protected virtual void AfterFluidOut(float fd)
        {

        }

        /// <summary>
        /// Excrete cums in womb naturally
        /// </summary>
        public void CumOut()
        {
            float leakfactor = 1.0f;
            float totalleak = 0f;
            float cumd = TotalCumPercent;
            List<string> filthlabels = new List<string>();
            BeforeCumOut(out Absorber absorber);
            if (cums.NullOrEmpty()) return;
            if (TotalCum > Props.maxCumCapacity * Pawn.BodySize) leakfactor = Math.Min(1 + (TotalCum - Props.maxCumCapacity * Pawn.BodySize) / 10, 2f);
            if (absorber != null && absorber.dirty && !absorber.LeakAfterDirty) leakfactor = 0f;
            if (Pawn.CurJobDef == xxx.knotted) leakfactor = 0f;
            List<Cum> removecums = new List<Cum>();
            foreach (Cum cum in cums)
            {
                cum.CumEffects(Pawn);
                float vd = cum.DismishNatural(leakfactor, this, antisperm);
                cum.MakeThinner(Configurations.CycleAcceleration);
                totalleak += AbsorbCum(vd, absorber);
                string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
                filthlabels.Add(tmp.Replace(" x1", ""));
                if (cum.ShouldRemove()) removecums.Add(cum);
            }
            if (cums.Count > 1) MakeCumFilthMixture(totalleak, filthlabels);
            else if (cums.Count == 1) MakeCumFilth(cums.First(), totalleak);
            foreach (Cum cum in removecums)
            {
                cums.Remove(cum);
            }
            cumd = TotalCumPercent - cumd;
            if (totalleak >= 1.0f) AfterCumOut();
            AfterFluidOut(cumd);

        }

        /// <summary>
        /// Force excrete cums in womb and get excreted amount of specific cum.
        /// </summary>
        /// <param name="targetcum"></param>
        /// <param name="portion"></param>
        /// <returns>Amount of target cum</returns>
        public float CumOut(Cum targetcum, float portion = 0.1f)
        {
            if (cums.NullOrEmpty()) return 0;
            float totalleak = 0;
            List<string> filthlabels = new List<string>();
            float outcum = 0;
            float cumd = TotalCumPercent;
            List<Cum> removecums = new List<Cum>();
            foreach (Cum cum in cums)
            {
                float vd = cum.DismishForce(portion);
                if (cum.Equals(targetcum)) outcum = vd;
                //MakeCumFilth(cum, vd - cum.volume);
                string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
                filthlabels.Add(tmp.Replace(" x1", ""));
                totalleak += vd;
                if (cum.ShouldRemove()) removecums.Add(cum);
            }
            if (cums.Count > 1) MakeCumFilthMixture(totalleak, filthlabels);
            else if (cums.Count == 1) MakeCumFilth(cums.First(), totalleak);
            foreach (Cum cum in removecums)
            {
                cums.Remove(cum);
            }
            cumd = TotalCumPercent - cumd;
            AfterFluidOut(cumd);
            return outcum;
        }

        /// <summary>
        /// Force excrete cums in womb and get mixture of cum.
        /// </summary>
        /// <param name="mixtureDef"></param>
        /// <param name="portion"></param>
        /// <returns></returns>
        public CumMixture MixtureOut(ThingDef mixtureDef, float portion = 0.1f)
        {
            if (cums.NullOrEmpty()) return null;
            Color color = GetCumMixtureColor;
            float totalleak = 0;
            List<string> cumlabels = new List<string>();
            List<Cum> removecums = new List<Cum>();
            bool pure = true;
            foreach (Cum cum in cums)
            {
                float vd = cum.DismishForce(portion);
                string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
                cumlabels.Add(tmp.Replace(" x1", ""));
                totalleak += vd;
                if (cum.ShouldRemove()) removecums.Add(cum);
                if (cum.notcum) pure = false;
            }
            foreach (Cum cum in removecums)
            {
                cums.Remove(cum);
            }
            return new CumMixture(Pawn, totalleak, cumlabels, color, mixtureDef, pure);
        }

        /// <summary>
        /// Debug: Remove all cums from a womb
        /// </summary>
        /// <returns></returns>
        public void RemoveAllCums()
        {
            cums.Clear();
        }

        /// <summary>
        /// Fertilize eggs and return the result
        /// </summary>
        /// <returns></returns>
        protected void FertilizationCheck()
        {
            if (eggs.NullOrEmpty()) return;
            foreach (Egg egg in eggs)
            {
                if (!egg.fertilized) egg.fertilizer = Fertilize();
                if (egg.fertilizer != null)
                {
                    egg.fertilized = true;
                }
            }
        }

        public void Initialize()
        {
            initError = true;
            Props = (CompProperties_Menstruation)props;

            if (Props.infertile)
            {
                if (cums == null) cums = new List<Cum>();
                curStage = Stage.None;
                loaded = true;
                initError = false;
                return;
            }

            if (cycleSpeed < 0f) cycleSpeed = Utility.RandGaussianLike(0.8f, 1.2f);
            if (cycleVariability < 0f) cycleVariability = MenstruationUtility.RandomVariabilityPercent();
            if (currentIntervalHours < 0)
            {
                if (Pawn.health.capacities.GetLevel(xxx.reproduction) <= 0) curStage = Stage.Young;
                else if (!IsBreedingSeason()) curStage = Stage.Anestrus;
                else curStage = RandomStage();
                if (curStage == Stage.Follicular)
                    currentIntervalHours = PeriodRandomizer(Stage.Follicular) - PeriodRandomizer(Stage.Bleeding);
                else
                    currentIntervalHours = PeriodRandomizer(curStage);
                if (currentIntervalHours <= 0) currentIntervalHours = 1;
                else if (currentIntervalHours < curStageHrs) curStageHrs = currentIntervalHours;
            }
            if (crampPain < 0) crampPain = PainRandomizer();
            if (cums == null) cums = new List<Cum>();
            if (eggs == null) eggs = new List<Egg>();

            InitOvary();
            TakeLoosePregnancy();

            //Log.Message(Pawn.Label + " - Initialized menstruation comp");
            loaded = true;
            initError = false;
        }

        protected virtual float RaceCyclesPerYear()
        {
            int breedingSeasons = 0;
            if (Props.breedingSeason == SeasonalBreed.Always) breedingSeasons = 4;
            else
            {
                if ((Props.breedingSeason & SeasonalBreed.Spring) != 0) breedingSeasons++;
                if ((Props.breedingSeason & SeasonalBreed.Summer) != 0) breedingSeasons++;
                if ((Props.breedingSeason & SeasonalBreed.Fall) != 0) breedingSeasons++;
                if ((Props.breedingSeason & SeasonalBreed.Winter) != 0) breedingSeasons++;
            }
            float breedingRatio = breedingSeasons / 4.0f;
            return breedingRatio * GenDate.DaysPerYear / ((float)(Props.follicularIntervalDays + Props.lutealIntervalDays) / Configurations.CycleAccelerationDefault);
        }

        protected virtual int PawnEggsUsed(float pawnCyclesElapsed, float avglittersize)
        {
            return (int)(pawnCyclesElapsed * avglittersize);
        }

        public int GetOvaryPowerByAge()
        {
            float avglittersize;
            try
            {
                avglittersize = Mathf.Max(Rand.ByCurveAverage(Pawn.def.race.litterSizeCurve), 1.0f);
            }
            catch (NullReferenceException)
            {
                avglittersize = 1.0f;
            }

            float fertStartAge = Pawn.RaceProps.lifeStageAges?.Find(stage => stage.def.reproductive)?.minAge ?? 0.0f;
            float fertEndAge = Pawn.RaceProps.lifeExpectancy * (Pawn.IsAnimal() ? RJWPregnancySettings.fertility_endage_female_animal : RJWPregnancySettings.fertility_endage_female_humanlike);
            if (fertEndAge < fertStartAge) fertEndAge = fertStartAge;

            float raceCyclesPerYear = RaceCyclesPerYear();
            int lifetimeCycles = (int)(raceCyclesPerYear * (fertEndAge - fertStartAge));
            int lifetimeEggs = (int)(lifetimeCycles * avglittersize * Props.eggMultiplier * Utility.RandGaussianLike(0.70f, 1.30f, 5));

            float pawnCyclesPerYear = raceCyclesPerYear * cycleSpeed;
            float pawnCyclesElapsed = Mathf.Max((Pawn.ageTracker.AgeBiologicalYearsFloat - fertStartAge) * pawnCyclesPerYear, 0.0f);
            int pawnEggsUsed = PawnEggsUsed(pawnCyclesElapsed, avglittersize);

            return Math.Max(lifetimeEggs - pawnEggsUsed, 0);
        }

        protected void InitOvary()
        {
            if (ovarypower < -50000)
            {
                ovarypower = GetOvaryPowerByAge();
                if (Props.infertile) curStage = Stage.None;
                else if (ovarypower < 1)
                {
                    curStage = Stage.Young;
                }
            }
        }

        public void RecoverOvary(float multiply = 1.2f)
        {
            ovarypower = Math.Max(0, (int)(ovarypower * multiply));
        }

        protected void AfterSimulator()
        {
            if (EggHealth < 1f)
            {
                if (sexNeed == null) sexNeed = Pawn.needs.TryGetNeed(VariousDefOf.SexNeed);
                if (sexNeed?.CurLevel < 0.5) sexNeed.CurLevel += 0.01f / Math.Max(1, Pawn.GetMenstruationComps().Count());
            }
        }

        protected virtual bool ShouldBeInEstrus()
        {
            if (!loaded)
                Initialize();
            switch (curStage)
            {
                case Stage.Follicular:
                case Stage.ClimactericFollicular:
                    return curStageHrs > currentIntervalHours - Props.estrusDaysBeforeOvulation * 24;
                case Stage.Ovulatory:
                    return true;
                case Stage.Luteal:
                case Stage.ClimactericLuteal:
                    return curStageHrs < Props.eggLifespanDays * 24;
                default:
                    return false;
            }
        }

        public EstrusLevel GetEstrusLevel()
        {
            if (!ShouldBeInEstrus()) return EstrusLevel.None;
            else return Props.concealedEstrus ? EstrusLevel.Concealed : EstrusLevel.Visible;
        }

        public void SetEstrus()
        {
            Hediff hediff = HediffMaker.MakeHediff(Props.concealedEstrus ? VariousDefOf.Hediff_Estrus_Concealed : VariousDefOf.Hediff_Estrus, Pawn);
            Pawn.health.AddHediff(hediff);
        }

        public bool IsBreedingSeason()
        {
            if (Props.breedingSeason == SeasonalBreed.Always) return true;

            int tile = Pawn.Tile;
            if (tile < 0) tile = Find.AnyPlayerHomeMap?.Tile ?? -1;
            if (tile < 0) return true;
            switch (GenLocalDate.Season(tile))
            {
                case Season.Spring:
                    return (Props.breedingSeason & SeasonalBreed.Spring) != 0;
                case Season.Summer:
                case Season.PermanentSummer:
                    return (Props.breedingSeason & SeasonalBreed.Summer) != 0;
                case Season.Fall:
                    return (Props.breedingSeason & SeasonalBreed.Fall) != 0;
                case Season.Winter:
                case Season.PermanentWinter:
                    return (Props.breedingSeason & SeasonalBreed.Winter) != 0;
                default:
                    return false;
            }
        }

        protected Pawn Fertilize()
        {
            if (cums.NullOrEmpty()) return null;
            List<Cum> eligibleCum = cums.FindAll(cum => !cum.notcum && cum.FertVolume > 0 && cum.pawn != null && (RJWPregnancySettings.bestial_pregnancy_enabled || xxx.is_animal(Pawn) == xxx.is_animal(cum.pawn)));
            if (eligibleCum.Count == 0) return null;

            float totalFertPower = eligibleCum.Sum(cum => cum.FertVolume);

            if (Rand.Range(0.0f, 1.0f) > 1.0f - Mathf.Pow(1.0f - Configurations.FertilizeChance, totalFertPower * Props.basefertilizationChanceFactor))
                return null;

            Pawn.records.AddTo(VariousDefOf.AmountofFertilizedEggs, 1);
            float selection = Rand.Range(0.0f, totalFertPower);

            foreach (Cum cum in eligibleCum)
            {
                selection -= cum.FertVolume;
                if (selection <= 0) return cum.pawn;
            }

            // We shouldn't reach here, but floating point errors exist, so just to be sure, select whomever came the most
            return eligibleCum.MaxBy(cum => cum.FertVolume).pawn;
        }

        protected bool Implant()
        {
            if (eggs.NullOrEmpty()) return false;

            List<Egg> deadeggs = new List<Egg>();
            bool pregnant = false;
            foreach (Egg egg in eggs)
            {
                if (!egg.fertilized ||
                    egg.fertstage < minImplantAgeHours ||
                    egg.position < Math.Min(Props.lutealIntervalDays * 24 / 2, maxImplantDelayHours))
                    continue;
                else if (egg.fertilizer == null)
                {
                    if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to null father");
                    deadeggs.Add(egg);
                    continue;
                }
                else if (Pawn.health.hediffSet.GetHediffs<Hediff_InsectEgg>().Any() || pregnancy is Hediff_MechanoidPregnancy)
                {
                    if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to insect or mechanoid pregnancy");
                    deadeggs.Add(egg);
                    continue;
                }
                else if (Rand.Range(0.0f, 1.0f) <= Configurations.ImplantationChance * ImplantFactor * InterspeciesImplantFactor(egg.fertilizer))
                {
                    if (Configurations.Debug) Log.Message($"Implanting fertilized egg of {Pawn} into {parent}, father {egg.fertilizer}");
                    if (pregnancy != null)
                    {
                        if (Configurations.UseMultiplePregnancy && Configurations.EnableHeteroOvularTwins)
                        {
                            if (pregnancy is Hediff_MultiplePregnancy h)
                            {
                                if (Configurations.Debug) Log.Message($"Adding to existing pregnancy {h}");
                                h.AddNewBaby(Pawn, egg.fertilizer);
                            }
                            pregnant = true;
                            deadeggs.Add(egg);
                        }
                        else
                        {
                            pregnant = true;
                            break;
                        }
                    }
                    else
                    {
                        if (!Configurations.UseMultiplePregnancy)
                        {
                            if (Configurations.Debug) Log.Message($"Creating new base RJW pregnancy");
                            PregnancyHelper.PregnancyDecider(Pawn, egg.fertilizer);
                            // I hate having to do this, but it gets the newest pregnancy
                            pregnancy = Pawn.health.hediffSet.GetHediffs<Hediff_BasePregnancy>().MaxBy(hediff => hediff.loadID);
                            pregnant = true;
                            break;
                        }
                        else
                        {
                            if (Configurations.Debug) Log.Message($"Creating new pregnancy");
                            pregnancy = Hediff_BasePregnancy.Create<Hediff_MultiplePregnancy>(Pawn, egg.fertilizer);
                            pregnant = true;
                            deadeggs.Add(egg);
                        }
                        pregnancy.p_start_tick -= egg.fertstage / Configurations.CycleAcceleration * GenDate.TicksPerHour;
                        pregnancy.p_end_tick -= egg.fertstage / Configurations.CycleAcceleration * GenDate.TicksPerHour;
                    }

                }
                else
                {
                    if (Configurations.Debug) Log.Message($"Fertilized egg of {Pawn} failed to implant (father {egg.fertilizer})");
                    deadeggs.Add(egg);
                }
            }

            if (pregnant && (!Configurations.UseMultiplePregnancy || !Configurations.EnableHeteroOvularTwins))
            {
                eggs.Clear();
                return true;
            }
            else
            {
                foreach (Egg egg in deadeggs)
                {
                    eggs.Remove(egg);
                }
            }
            return pregnant;
        }

        protected void BleedOut()
        {
            CumIn(Pawn, Rand.Range(0.02f * Configurations.BleedingAmount, 0.04f * Configurations.BleedingAmount), Translations.Menstrual_Blood, -5.0f, Pawn.def.race?.BloodDef ?? ThingDefOf.Filth_Blood);
            Cum blood = GetNotCum(Translations.Menstrual_Blood);
            if (blood != null) blood.Color = BloodColor;
        }

        /// <summary>
        /// Make filth ignoring absorber
        /// </summary>
        /// <param name="cum"></param>
        /// <param name="amount"></param>
        protected void MakeCumFilth(Cum cum, float amount)
        {
            if (Pawn.Map == null) return;
            if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn?.LabelShort ?? "Unknown");
        }

        /// <summary>
        /// Absorb cum and return leaked amount
        /// </summary>
        /// <param name="amount"></param>
        /// <param name="absorber"></param>
        ///
        /// <returns></returns>
        protected float AbsorbCum(float amount, Absorber absorber)
        {

            if (absorber == null)
            {
                //if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort);
                return amount;
            }

            float absorbable = absorber.GetStatValue(VariousDefOf.MaxAbsorbable);
            absorber.SetColor(Colors.CMYKLerp(GetCumMixtureColor, absorber.DrawColor, 1f - amount / absorbable));
            if (absorber.dirty)
            {
                //if (absorber.LeakAfterDirty) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort);
                return amount;
            }
            absorber.absorbedfluids += amount;
            if (absorber.absorbedfluids > absorbable && !Pawn.apparel.IsLocked(absorber))
            {
                absorber.def = absorber.DirtyDef;
                //absorber.fluidColor = GetCumMixtureColor;
                absorber.dirty = true;
            }
            return 0;
        }

        protected float MakeCumFilthMixture(float amount, List<string> cumlabels)
        {
            if (Pawn.Map == null) return 0;
            if (amount >= minmakefilthvalue)
            {
                FilthMaker_Colored.TryMakeFilth(Pawn.Position, Pawn.Map, VariousDefOf.FilthMixture, cumlabels, GetCumMixtureColor, false);
            }
            return amount;
        }

        protected void EggDecay()
        {
            List<Egg> deadeggs = new List<Egg>();
            foreach (Egg egg in eggs)
            {
                egg.position += Configurations.CycleAcceleration;
                if (egg.fertilized) egg.fertstage += Configurations.CycleAcceleration;
                else
                {
                    egg.lifespanhrs -= Configurations.CycleAcceleration;
                    if (egg.lifespanhrs < 0) deadeggs.Add(egg);
                }
            }
            foreach (Egg egg in deadeggs)
            {
                eggs.Remove(egg);
            }
        }

        protected void AddCrampPain()
        {
            Hediff hediff = HediffMaker.MakeHediff(VariousDefOf.Hediff_MenstrualCramp, Pawn);
            hediff.Severity = crampPain * Rand.Range(0.9f, 1.1f);
            HediffCompProperties_SeverityPerDay Prop = (HediffCompProperties_SeverityPerDay)hediff.TryGetComp<HediffComp_SeverityPerDay>().props;
            Prop.severityPerDay = -hediff.Severity / (currentIntervalHours / 24) * Configurations.CycleAcceleration;
            Pawn.health.AddHediff(hediff, Genital_Helper.get_genitalsBPR(Pawn));
        }

        protected virtual void FollicularAction()
        {
            if (!IsBreedingSeason())
            {
                estrusflag = false;
                GoNextStage(Stage.Anestrus);
                return;
            }
            else if (curStageHrs >= currentIntervalHours)
            {
                GoOvulatoryStage();
            }
            else
            {
                curStageHrs += Configurations.CycleAcceleration;
                if (!estrusflag && curStageHrs > currentIntervalHours - Props.estrusDaysBeforeOvulation * 24)
                {
                    estrusflag = true;
                    SetEstrus();
                }
                StayCurrentStage();
            }
        }

        protected virtual void OvulatoryAction()
        {
            estrusflag = false;
            int eggnum;
            try
            {
                eggnum = Math.Max((int)Rand.ByCurve(Pawn.def.race.litterSizeCurve), 1);
            }
            catch (NullReferenceException)
            {
                eggnum = 1;
            }
            catch (ArgumentException e)
            {
                Log.Warning($"Invalid litterSizeCurve for {Pawn.def}: {e}");
                eggnum = 1;
            }
            eggnum += eggstack;

            for (int i = 0; i < eggnum; i++)
                eggs.Add(new Egg((int)(Props.eggLifespanDays * 24 / CycleFactor)));
            ovarypower -= eggnum;

            eggstack = 0;
            if (EggHealth <= 0)
            {
                eggs.Clear();
                ovarypower = 0;
                GoNextStage(Stage.Young);
            }
            else
            {
                GoNextStage(Stage.Luteal);
            }
        }

        protected virtual void LutealAction()
        {
            if (curStageHrs > currentIntervalHours)
            {
                eggs.Clear();
                if (EggHealth < 1f / 4f || (EggHealth < 1f / 3f && Rand.Range(0.0f, 1.0f) < 0.3f)) //skips bleeding
                {
                    GoNextStage(Stage.Follicular);
                }
                else
                {
                    GoFollicularOrBleeding();
                }
            }
            else if (!eggs.NullOrEmpty())
            {
                FertilizationCheck();
                EggDecay();
                if (Implant())
                {
                    GoNextStage(Stage.Pregnant);
                }
                else
                {
                    curStageHrs += Configurations.CycleAcceleration;
                    StayCurrentStage();
                }
            }
            else
            {
                curStageHrs += Configurations.CycleAcceleration;
                StayCurrentStage();
            }

        }

        protected virtual void BleedingAction()
        {
            if (curStageHrs >= currentIntervalHours)
            {
                Hediff hediff = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_MenstrualCramp);
                if (hediff != null) Pawn.health.RemoveHediff(hediff);
                int totalFollicularHours = PeriodRandomizer(Stage.Follicular);  // The total amount of time for both bleeding and follicular
                if (totalFollicularHours <= currentIntervalHours)   // We've bled for so long that we completely missed the follicular phase
                    GoOvulatoryStage();
                else
                {
                    currentIntervalHours = totalFollicularHours - currentIntervalHours; // I.e., the remaining follicular hours equals the total minus the bleeding hours elapsed
                    GoNextStage(Stage.Follicular, false);
                }
            }
            else
            {
                if (curStageHrs < currentIntervalHours / 4) for (int i = 0; i < Configurations.CycleAcceleration; i++) BleedOut();
                curStageHrs += Configurations.CycleAcceleration;
                StayCurrentStage();
            }
        }

        protected virtual void PregnantAction()
        {
            if (!eggs.NullOrEmpty())
            {
                FertilizationCheck();
                EggDecay();
                Implant();
            }

            if (pregnancy != null && Pawn.health.hediffSet.hediffs.Contains(pregnancy))
            {
                curStageHrs += 1;
                StayCurrentStageConst(Stage.Pregnant);
            }
            else
            {
                if (pregnancy != null) pregnancy = null;
                GoNextStage(Stage.Recover);
            }
        }

        protected virtual void RecoverAction()
        {
            if (curStageHrs >= currentIntervalHours)
            {
                if (Pawn.health.capacities.GetLevel(xxx.reproduction) == 0 || EggHealth <= 0)
                {
                    GoNextStage(Stage.Young);
                }
                else if (!IsBreedingSeason())
                {
                    GoNextStage(Stage.Anestrus);
                }
                else
                {
                    GoNextStage(Stage.Follicular);
                }
            }
            else
            {
                curStageHrs += Configurations.CycleAcceleration;
                StayCurrentStage();
            }
        }

        protected virtual void YoungAction()
        {
            if (Pawn.health.capacities.GetLevel(xxx.reproduction) <= 0 || EggHealth <= 0)
            {
                StayCurrentStageConst(Stage.Young);
            }
            else
            {
                bool breedingSeason = IsBreedingSeason();
                GoNextStage(breedingSeason ? Stage.Follicular : Stage.Anestrus, breedingSeason);
            }
        }

        protected virtual void AnestrusAction()
        {
            if (IsBreedingSeason())
            {
                GoFollicularOrBleeding();
            }
            else
            {
                StayCurrentStage();
            }
        }

        protected virtual void ThoughtCumInside(Pawn cummer)
        {
            if (!xxx.is_human(Pawn) || !xxx.is_human(cummer)) return;

            if ((cummer.Has(Quirk.Teratophile) != (Pawn.GetStatValue(StatDefOf.PawnBeauty) >= 0)) ||
                cummer.Has(Quirk.ImpregnationFetish) ||
                cummer.Has(Quirk.Breeder))
            {
                if (cummer.relations.OpinionOf(Pawn) <= -25)
                {
                    cummer.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideM, Pawn);
                }
                else
                {
                    cummer.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideM, Pawn);
                }
            }

            if (IsDangerDay)
            {
                if (Pawn.Has(Quirk.Breeder) || Pawn.Has(Quirk.ImpregnationFetish))
                {
                    Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideFFetish, cummer);
                }
                else if (Pawn.relations.OpinionOf(cummer) <= -5)
                {
                    Pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(VariousDefOf.CameInsideF, cummer);
                    Pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(VariousDefOf.HaterCameInsideFEstrus, cummer);
                    Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideF, cummer);
                }
                else if (Pawn.IsInEstrus() && Pawn.relations.OpinionOf(cummer) < RJWHookupSettings.MinimumRelationshipToHookup)
                {
                    Pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(VariousDefOf.CameInsideF, cummer);
                    Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideFEstrus, cummer);
                }
                else if (!Pawn.relations.DirectRelationExists(PawnRelationDefOf.Spouse, cummer) && !Pawn.relations.DirectRelationExists(PawnRelationDefOf.Fiance, cummer))
                {
                    if (Pawn.health.capacities.GetLevel(xxx.reproduction) < 0.50f) Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideFLowFert, cummer);
                    else Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideF, cummer);
                }
            }
            else
            {
                if (Pawn.Has(Quirk.Breeder) || Pawn.Has(Quirk.ImpregnationFetish))
                {
                    Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideFFetishSafe, cummer);
                }
                else if (Pawn.relations.OpinionOf(cummer) <= -5)
                {
                    Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideFSafe, cummer);
                }
            }
        }

        protected virtual void TaleCumInside(Pawn cummer)
        {
            // Only make the tale for human-on-human, consentual sex. Otherwise the art just gets too hard to phrase properly
            if (!xxx.is_human(Pawn) || !xxx.is_human(cummer) || Pawn == cummer) return;
            if (Pawn.CurJobDef != xxx.casual_sex && Pawn.CurJobDef != xxx.gettin_loved) return;
            if (!(Pawn.IsColonist || Pawn.IsPrisonerOfColony) && !(cummer.IsColonist || cummer.IsPrisonerOfColony)) return;
            if (!IsDangerDay) return;
            TaleRecorder.RecordTale(VariousDefOf.TaleCameInside, new object[] { cummer, Pawn });
        }

        protected void GoNextStage(Stage nextstage, bool calculateHours = true)
        {
            curStageHrs = 0;
            if (calculateHours) currentIntervalHours = PeriodRandomizer(nextstage);
            curStage = nextstage;
        }

        protected virtual void GoOvulatoryStage()
        {
            if (EggHealth < 1.0f / 3.0f && Rand.Range(0.0f, 1.0f) < 0.2f)   // Skip ovulation if deep into climacteric
            {
                estrusflag = false;
                GoNextStage(Stage.Luteal);
            }
            else GoNextStage(Stage.Ovulatory);
        }

        //stage can be interrupted in other reasons
        protected void StayCurrentStage()
        {
        }

        //stage never changes
        protected void StayCurrentStageConst(Stage curstage)
        {
        }

        protected void GoFollicularOrBleeding()
        {
            if (Props.bleedingIntervalDays == 0)
            {
                GoNextStage(Stage.Follicular);
            }
            else
            {
                GoNextStage(Stage.Bleeding);
                if (crampPain >= 0.05f)
                {
                    AddCrampPain();
                }
            }
        }

        protected int PeriodRandomizer(Stage stage)
        {
            float variabilityFactor = (EggHealth < 1.0f) ? 6.0f : 1.0f;
            // Most cycle lengthening or shortening occurs in the follicular phase, so weight towards that
            switch (stage)
            {
                case Stage.Follicular:
                case Stage.ClimactericFollicular:
                    return (int)(Props.follicularIntervalDays * 24 * (1 + Rand.Range(-cycleVariability, cycleVariability) * 1.5f * variabilityFactor) / (1 + (cycleSpeed - 1) * 1.5f));
                case Stage.Luteal:
                case Stage.ClimactericLuteal:
                    return (int)(Props.lutealIntervalDays * 24 * (1 + Rand.Range(-cycleVariability, cycleVariability) * 0.5f * variabilityFactor) / (1 + (cycleSpeed - 1) * 0.5f));
                case Stage.Bleeding:
                case Stage.ClimactericBleeding:
                    return (int)(Props.bleedingIntervalDays * 24 * (1 + Rand.Range(-cycleVariability, cycleVariability) * 0.5f * variabilityFactor) / (1 + (cycleSpeed - 1) * 0.5f));
                case Stage.Recover:
                    return (int)(Props.recoveryIntervalDays * 24 * Rand.Range(0.95f, 1.05f));
                case Stage.Pregnant:
                    return (int)MenstruationUtility.GestationHours(pregnancy);
                default:    // Often unused
                    return 1;
            }
        }

        protected float InterspeciesImplantFactor(Pawn fertilizer)
        {
            if (fertilizer.def.defName == Pawn.def.defName) return 1.0f;
            else
            {
                if (RJWPregnancySettings.complex_interspecies) return SexUtility.BodySimilarity(Pawn, fertilizer);
                else return RJWPregnancySettings.interspecies_impregnation_modifier;
            }
        }

        protected float PainRandomizer()
        {
            float rand = Rand.Range(0.0f, 1.0f);
            if (rand < 0.01f) return Rand.Range(0.0f, 0.2f);
            else if (rand < 0.2f) return Rand.Range(0.1f, 0.2f);
            else if (rand < 0.8f) return Rand.Range(0.2f, 0.4f);
            else if (rand < 0.95f) return Rand.Range(0.4f, 0.6f);
            else return Rand.Range(0.6f, 1.0f);
        }

        protected Stage RandomStage()
        {
            Stage stage = Rand.ElementByWeight(
                Stage.Follicular, Props.follicularIntervalDays - Props.bleedingIntervalDays,
                Stage.Luteal, Props.lutealIntervalDays,
                Stage.Bleeding, Props.bleedingIntervalDays);

            switch (stage)
            {
                case Stage.Follicular:
                    curStageHrs = Rand.Range(0, (Props.follicularIntervalDays - Props.bleedingIntervalDays) * 24);
                    break;
                case Stage.Luteal:
                    curStageHrs = Rand.Range(0, Props.lutealIntervalDays * 24);
                    break;
                case Stage.Bleeding:
                    curStageHrs = Rand.Range(0, Props.bleedingIntervalDays * 24);
                    break;
            }
            return stage;
        }

        // Searches for an RJW pregnancy unclaimed by any womb and put it in this one
        public void TakeLoosePregnancy()
        {
            if (pregnancy != null) return;

            pregnancy =
                Pawn.health.hediffSet.GetHediffs<Hediff_BasePregnancy>().Except(
                    Pawn.GetMenstruationComps().Select(comp => comp.pregnancy).Where(preg => preg != null)
                    ).FirstOrDefault();
            if (pregnancy != null)
                GoNextStage(Stage.Pregnant);
        }

        public void CopyCycleProperties(HediffComp_Menstruation original)
        {
            cycleSpeed = original.cycleSpeed;
            cycleVariability = original.cycleVariability;
            ovarypower = original.ovarypower;
            crampPain = original.crampPain;
        }

        public class Egg : IExposable
        {
            public bool fertilized;
            public int lifespanhrs;
            public Pawn fertilizer;
            public int position;
            public int fertstage = 0;

            public Egg()
            {
                fertilized = false;
                lifespanhrs = (int)(96 * Configurations.EggLifespanMultiplier);
                fertilizer = null;
                position = 0;
            }

            public Egg(int lifespanhrs)
            {
                fertilized = false;
                this.lifespanhrs = (int)(lifespanhrs * Configurations.EggLifespanMultiplier);
                fertilizer = null;
                position = 0;
            }

            public void ExposeData()
            {
                Scribe_References.Look(ref fertilizer, "fertilizer", true);
                Scribe_Values.Look(ref fertilized, "fertilized", fertilized, true);
                Scribe_Values.Look(ref lifespanhrs, "lifespanhrs", lifespanhrs, true);
                Scribe_Values.Look(ref position, "position", position, true);
                Scribe_Values.Look(ref fertstage, "fertstage", fertstage, true);
            }
        }

    }

    public class HediffComp_Anus : HediffComp
    {
        protected float? originanussize;

        public float OriginAnusSize
        {
            get
            {
                if (originanussize == null)
                {
                    originanussize = parent.Severity;
                }
                return originanussize ?? 0.1f;
            }
        }

        public override void CompExposeData()
        {
            base.CompExposeData();
            Scribe_Values.Look(ref originanussize, "originanussize", originanussize, true);
        }

        public override void CompPostTick(ref float severityAdjustment)
        {
        }
    }
}


code:
using RimWorld;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
using Verse.AI;

namespace RJW_Menstruation
{
    public static class MenstruationUtility
    {
        [Obsolete("This method is obsolete. Use GetMenstruationComps or a related function instead", false)]
        public static HediffComp_Menstruation GetMenstruationComp(this Pawn pawn)
        {
            return pawn.GetFirstMenstruationComp();
        }

        public static IEnumerable<HediffComp_Menstruation> GetMenstruationComps(this Pawn pawn)
        {
            List<Hediff> hedifflist = Genital_Helper.get_PartsHediffList(pawn, Genital_Helper.get_genitalsBPR(pawn))?.FindAll(h => VariousDefOf.AllVaginas.Contains(h.def));
            if (hedifflist == null) yield break;
            foreach (Hediff hediff in hedifflist)
            {
                HediffComp_Menstruation result = hediff.TryGetComp<HediffComp_Menstruation>();
                if (result != null) yield return result;
            }
        }

        public static HediffComp_Menstruation GetFirstMenstruationComp(this Pawn pawn)
        {
            return pawn.GetMenstruationComps().FirstOrDefault();
        }

        public static HediffComp_Menstruation GetRandomMenstruationComp(this Pawn pawn)
        {
            return pawn.GetMenstruationComps().RandomElementWithFallback();
        }

        public static HediffComp_Menstruation GetFertileMenstruationComp(this Pawn pawn)
        {
            List<HediffComp_Menstruation> comps = pawn.GetMenstruationComps().ToList();
            return comps.Where(c => c.IsDangerDay).RandomElementWithFallback() ?? comps.RandomElementWithFallback();
        }

        public static HediffComp_Menstruation GetMenstruationComp(this Hediff vagina)
        {
            if (VariousDefOf.AllVaginas.Contains(vagina?.def))
            {
                return vagina.TryGetComp<HediffComp_Menstruation>();
            }
            return null;
        }

        public static HediffComp_Menstruation GetMenstruationComp(this Hediff_BasePregnancy pregnancy)
        {
            return pregnancy?.pawn.GetMenstruationComps().FirstOrDefault(comp => comp.Pregnancy == pregnancy);
        }

        public static HediffComp_Anus GetAnusComp(this Pawn pawn)
        {
            List<Hediff> hedifflist = Genital_Helper.get_PartsHediffList(pawn, Genital_Helper.get_genitalsBPR(pawn))?.FindAll((Hediff h) => h.def.defName.ToLower().Contains("anus"));
            HediffComp_Anus result;
            if (!hedifflist.NullOrEmpty())
            {
                foreach (Hediff h in hedifflist)
                {
                    result = h.TryGetComp<HediffComp_Anus>();
                    if (result != null) return result;
                }
            }
            return null;
        }

        public static HediffComp_Anus GetAnusComp(this Hediff hediff)
        {
            if (hediff is Hediff_PartBaseNatural || hediff is Hediff_PartBaseArtifical)
            {
                return hediff.TryGetComp<HediffComp_Anus>();
            }
            return null;
        }

        public static float GetFertilityChance(this HediffComp_Menstruation comp)
        {
            return 1.0f - Mathf.Pow(1.0f - Configurations.FertilizeChance, comp.TotalFertCum * comp.Props.basefertilizationChanceFactor);
        }

        public static Texture2D GetPregnancyIcon(this HediffComp_Menstruation comp, Hediff hediff)
        {
            string icon = "";
            int babycount = 1;
            if (hediff is Hediff_MechanoidPregnancy)
            {
                return ContentFinder<Texture2D>.Get(("Womb/Mechanoid_Fluid"), true);
            }
            else if (hediff is Hediff_BasePregnancy h)
            {
                babycount = h.babies.Count;
                string fetustex = h.babies?.FirstOrDefault()?.def.GetModExtension<PawnDNAModExtension>()?.fetusTexPath ?? "Fetus/Fetus_Default";
                if (h.GestationProgress < 0.2f) icon = comp.WombTex + "_Implanted";
                else if (h.GestationProgress < 0.3f)
                {
                    if (h.babies?.First()?.def?.race?.FleshType == FleshTypeDefOf.Insectoid) icon += "Fetus/Insects/Insect_Early00";
                    else icon += "Fetus/Fetus_Early00";
                }
                else if (h.GestationProgress < 0.4f) icon += fetustex + "00";
                else if (h.GestationProgress < 0.5f) icon += fetustex + "01";
                else if (h.GestationProgress < 0.6f) icon += fetustex + "02";
                else if (h.GestationProgress < 0.7f) icon += fetustex + "03";
                else if (h.GestationProgress < 0.8f) icon += fetustex + "04";
                else icon += fetustex + "05";
            }
            else icon = "Fetus/Slime_Abomi02";

            Texture2D result = TryGetTwinsIcon(icon, babycount);

            if (result == null) result = ContentFinder<Texture2D>.Get((icon), true);
            return result;
        }

        public static Texture2D TryGetTwinsIcon(string path, int babycount)
        {
            for (int i = babycount; i > 1; i--)
            {
                Texture2D result = ContentFinder<Texture2D>.Get(path + "_Multiplet_" + i, false);
                if (result != null) return result;
            }
            return null;
        }

        public static Texture2D GetCumIcon(this HediffComp_Menstruation comp)
        {
            string icon = comp.WombTex;
            float cumpercent = comp.TotalCumPercent;
            if (cumpercent < 0.001f) return ContentFinder<Texture2D>.Get("Womb/Empty", true);
            else if (cumpercent < 0.01f) icon += "_Cum_00";
            else if (cumpercent < 0.05f) icon += "_Cum_01";
            else if (cumpercent < 0.11f) icon += "_Cum_02";
            else if (cumpercent < 0.17f) icon += "_Cum_03";
            else if (cumpercent < 0.23f) icon += "_Cum_04";
            else if (cumpercent < 0.29f) icon += "_Cum_05";
            else if (cumpercent < 0.35f) icon += "_Cum_06";
            else if (cumpercent < 0.41f) icon += "_Cum_07";
            else if (cumpercent < 0.47f) icon += "_Cum_08";
            else if (cumpercent < 0.53f) icon += "_Cum_09";
            else if (cumpercent < 0.59f) icon += "_Cum_10";
            else if (cumpercent < 0.65f) icon += "_Cum_11";
            else if (cumpercent < 0.71f) icon += "_Cum_12";
            else if (cumpercent < 0.77f) icon += "_Cum_13";
            else if (cumpercent < 0.83f) icon += "_Cum_14";
            else if (cumpercent < 0.89f) icon += "_Cum_15";
            else if (cumpercent < 0.95f) icon += "_Cum_16";
            else icon += "_Cum_17";
            Texture2D cumtex = ContentFinder<Texture2D>.Get((icon), true);
            return cumtex;
        }
        public static Texture2D GetInsectEggedIcon(this HediffComp_Menstruation comp)
        {
            List<Hediff_InsectEgg> hediffs = comp.Pawn.health.hediffSet.GetHediffs<Hediff_InsectEgg>().ToList();
            if (hediffs.NullOrEmpty()) return null;
            string path = "Womb/Insect_Egged/";
            if (hediffs.Max(hediff => hediff.eggssize) > 0.3)   // The threshold for "large egg" in the debug
            {
                if (hediffs.Count == 1) return ContentFinder<Texture2D>.Get(path + "Womb_Egged_Large", true);
                else return ContentFinder<Texture2D>.Get(path + "Womb_Egged_ManyMixed", true);
            }
            Texture2D result = ContentFinder<Texture2D>.Get(path + "Womb_Egged_" + hediffs.Count(), false);
            if (result == null) result = ContentFinder<Texture2D>.Get(path + "Womb_Egged_Many", true);

            return result;
        }
        public static Texture2D GetWombIcon(this HediffComp_Menstruation comp)
        {
            Texture2D wombtex = comp.GetInsectEggedIcon();
            if (wombtex != null) return wombtex;
            string icon = comp.WombTex;
            HediffComp_Menstruation.Stage stage = comp.curStage;
            if (stage == HediffComp_Menstruation.Stage.Bleeding || stage == HediffComp_Menstruation.Stage.ClimactericBleeding) icon += "_Bleeding";

            wombtex = ContentFinder<Texture2D>.Get(icon, true);

            return wombtex;
        }
        public static Texture2D GetEggIcon(this HediffComp_Menstruation comp, bool includeOvary)
        {
            switch (comp.CurrentVisibleStage)
            {
                case HediffComp_Menstruation.Stage.Follicular:
                case HediffComp_Menstruation.Stage.ClimactericFollicular:
                    if (!includeOvary) break;
                    if (comp is HediffComp_InducedOvulator)
                    {
                        if (comp.Pawn.jobs.curDriver is JobDriver_Sex job &&
                            job.Sexprops != null &&
                            !job.Sexprops.usedCondom &&
                            (job.Sexprops.sexType == xxx.rjwSextype.Vaginal || job.Sexprops.sexType == xxx.rjwSextype.DoublePenetration))
                            return ContentFinder<Texture2D>.Get("Ovaries/Ovary_01", true);
                        else break;
                    }
                    if (comp.curStageHrs > comp.CurStageIntervalHours - 30)   // Approximate time for ovulation to occur
                        return ContentFinder<Texture2D>.Get("Ovaries/Ovary_01", true);
                    else break;
                case HediffComp_Menstruation.Stage.Ovulatory:
                    if (!includeOvary) break;
                    return ContentFinder<Texture2D>.Get("Ovaries/Ovary_02", true);
                case HediffComp_Menstruation.Stage.Luteal:
                case HediffComp_Menstruation.Stage.ClimactericLuteal:
                    if (!comp.IsEggExist) break;
                    int fertstage = comp.IsFertilized;
                    if (fertstage >= 0)
                    {
                        if (fertstage <= Configurations.CycleAcceleration) return ContentFinder<Texture2D>.Get("Eggs/Egg_Fertilizing02", true);
                        else if (fertstage <= 18) return ContentFinder<Texture2D>.Get("Eggs/Egg_Fertilized00", true);
                        else if (fertstage <= 54) return ContentFinder<Texture2D>.Get("Eggs/Egg_Fertilized01", true);
                        else return ContentFinder<Texture2D>.Get("Eggs/Egg_Fertilized02", true);
                    }
                    else if (comp.IsEggFertilizing)
                    {
                        if (comp.GetFertilityChance() < 0.5f)
                            return ContentFinder<Texture2D>.Get("Eggs/Egg_Fertilizing00", true);
                        else
                            return ContentFinder<Texture2D>.Get("Eggs/Egg_Fertilizing01", true);
                    }
                    else return ContentFinder<Texture2D>.Get("Eggs/Egg", true);
                case HediffComp_Menstruation.Stage.Pregnant:
                    if (comp.Pregnancy is Hediff_MechanoidPregnancy) break;
                    else if (comp.GetPregnancyProgress() < 0.2f) return ContentFinder<Texture2D>.Get("Eggs/Egg_Implanted00", true);
                    else break;
            }
            return ContentFinder<Texture2D>.Get("Womb/Empty", true);
        }

        public static void DrawEggOverlay(this HediffComp_Menstruation comp, Rect wombRect, bool includeOvary)
        {
            Rect rect = new Rect(wombRect.xMax - wombRect.width / 3, wombRect.y, wombRect.width / 3, wombRect.width / 3);
            GUI.color = Color.white;
            GUI.DrawTexture(rect, comp.GetEggIcon(includeOvary), ScaleMode.ScaleToFit);
        }

        public static Texture2D GetGenitalIcon(this Pawn pawn, HediffComp_Menstruation comp, bool drawOrigin = false)
        {
            Hediff hediff = comp?.parent;
            if (hediff == null) return ContentFinder<Texture2D>.Get("Genitals/Vagina00", true);
            //HediffComp_Menstruation comp = hediff.GetMenstruationComp();
            string icon;
            float severity;
            if (drawOrigin) severity = comp.OriginVagSize;
            else severity = hediff.Severity;
            if (comp != null) icon = comp.VagTex;
            else icon = "Genitals/Vagina";

            if (severity < 0.20f) icon += "00";        //micro
            else if (severity < 0.30f) icon += "01";   //tight
            else if (severity < 0.40f) icon += "02";   //tight
            else if (severity < 0.47f) icon += "03";   //average
            else if (severity < 0.53f) icon += "04";   //average
            else if (severity < 0.60f) icon += "05";   //average
            else if (severity < 0.70f) icon += "06";   //accomodating
            else if (severity < 0.80f) icon += "07";   //accomodating
            else if (severity < 0.87f) icon += "08";   //cavernous
            else if (severity < 0.94f) icon += "09";   //cavernous
            else if (severity < 1.01f) icon += "10";   //cavernous
            else icon += "11";                                //abyssal

            return ContentFinder<Texture2D>.Get((icon), true);
        }

        public static Texture2D GetAnalIcon(this Pawn pawn, bool drawOrigin = false)
        {
            Hediff hediff = Genital_Helper.get_PartsHediffList(pawn, Genital_Helper.get_anusBPR(pawn)).FirstOrDefault(h => VariousDefOf.AllAnuses.Contains(h.def));
            if (hediff == null) hediff = Genital_Helper.get_PartsHediffList(pawn, Genital_Helper.get_anusBPR(pawn)).FirstOrDefault(h => h.def.defName.ToLower().Contains("anus"));
            if (hediff == null) return ContentFinder<Texture2D>.Get(("Genitals/Anal00"), true);

            string icon;
            float severity;
            HediffComp_Anus comp = hediff.GetAnusComp();
            if (comp != null)
            {
                CompProperties_Anus Props = (CompProperties_Anus)comp.props;
                icon = Props.analTex ?? "Genitals/Anal";
                if (drawOrigin) severity = comp.OriginAnusSize;
                else severity = hediff.Severity;
            }
            else
            {
                icon = "Genitals/Anal";
                severity = hediff.Severity;
            }
            if (severity < 0.20f) icon += "00";        //micro
            else if (severity < 0.40f) icon += "01";   //tight
            else if (severity < 0.60f) icon += "02";   //average
            else if (severity < 0.80f) icon += "03";   //accomodating
            else if (severity < 1.01f) icon += "04";   //cavernous
            else icon += "05";                                //abyssal

            return ContentFinder<Texture2D>.Get((icon), true);
        }

        public static float GestationHours(this Hediff_BasePregnancy hediff)
        {
            if (hediff == null)
            {
                Log.Error("Tried to get gestation length without a pregnancy.");
                return 1f;
            }
            else return (hediff.p_end_tick - hediff.p_start_tick) / GenDate.TicksPerHour;
        }

        public static float RandomVariabilityPercent(int recursion = 0)
        {
            // Humans, in days
            const float mean = 1.635f;
            const float stddev = 0.9138f;
            const float lambda = 0.234f;
            if (recursion >= 10) return mean / (28 * 2);

            float variability = Rand.Gaussian(mean, stddev) - Mathf.Log(Rand.Value) / lambda;
            variability /= 28 * 2;  // Convert to percentage

            if (variability < 0 || variability > 0.35f) return RandomVariabilityPercent(recursion + 1); // ~2% chance, about the limit on how far variability can go before things start to break
            else return variability;
        }

        public static bool IsInEstrus(this Pawn pawn, bool visible = true)
        {
            if (pawn.Dead) return false;
            return pawn.health?.hediffSet?.HasHediff(visible ? VariousDefOf.Hediff_Estrus : VariousDefOf.Hediff_Estrus_Concealed) ?? false;
        }

        public static HediffComp_Menstruation.EstrusLevel HighestEstrus(this Pawn pawn)
        {
            HediffComp_Menstruation.EstrusLevel res = HediffComp_Menstruation.EstrusLevel.None;

            foreach(HediffComp_Menstruation comp in pawn.GetMenstruationComps())
            {
                switch (comp.GetEstrusLevel())
                {
                    case HediffComp_Menstruation.EstrusLevel.None:
                        break;
                    case HediffComp_Menstruation.EstrusLevel.Concealed:
                        res = HediffComp_Menstruation.EstrusLevel.Concealed;
                        break;
                    case HediffComp_Menstruation.EstrusLevel.Visible:
                        return HediffComp_Menstruation.EstrusLevel.Visible;
                }
            }

            return res;
        }
    }
}

license: unknown


code:
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using Verse;

namespace RJW_Menstruation
{
    public class Hediff_Estrus : HediffWithComps
    {
        const int checkInterval = GenTicks.TickRareInterval;
        private bool shouldRemove = false;

        public override bool ShouldRemove
        {
            get
            {
                return shouldRemove || base.ShouldRemove;
            }
        }

        protected bool IsVisible
        {
            get
            {
                return def == VariousDefOf.Hediff_Estrus;
            }
        }

        public override void PostAdd(DamageInfo? dinfo)
        {
            base.PostAdd(dinfo);
            if (IsVisible)
            {
                List<Hediff> removals = pawn.health.hediffSet.hediffs.Where(hediff => hediff.def == VariousDefOf.Hediff_Estrus_Concealed).ToList();
                foreach (Hediff concealedEstrus in removals)
                {
                    pawn.health.RemoveHediff(concealedEstrus);
                }
            }
        }

        public override void PostTick()
        {
            base.PostTick();
            if (!pawn.IsHashIntervalTick(checkInterval)) return;

            if (IsVisible)
            {
                switch (pawn.HighestEstrus())
                {
                    case HediffComp_Menstruation.EstrusLevel.None:
                        shouldRemove = true;
                        break;
                    case HediffComp_Menstruation.EstrusLevel.Concealed:
                        shouldRemove = true;
                        pawn.health.AddHediff(VariousDefOf.Hediff_Estrus_Concealed);
                        break;
                    case HediffComp_Menstruation.EstrusLevel.Visible:
                        break;
                }
            }
            else
            {
                // Adding a visible will remove this one, so we don't have to check it here
                if (pawn.HighestEstrus() == HediffComp_Menstruation.EstrusLevel.None) shouldRemove = true;
            }
        }

        public override bool TryMergeWith(Hediff other)
        {
            if (!(other is Hediff_Estrus otherEstrus)) return false;

            if (this.def == otherEstrus.def) return true;
            else if (!this.IsVisible && otherEstrus.IsVisible && !otherEstrus.shouldRemove) return true; // A concealed estrus won't be added if there's a visible
            // This is a visible estrus overwriting a concealed.
            else return false;  // Since this is being added while hediffs are being looped through, it's not safe to remove the concealed yet. Do it in PostAdd.
        }
    }
}