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 ovulationIntervalHours = 12; //between the end of follicular and the egg emerging
        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 maxImplantDelayHours = 30 * GenDate.HoursPerDay;
        const int minImplantAgeHours = 3 * GenDate.HoursPerDay;
        const float fluidLeakThreshold = 3.5f;  // ml per filth produced
        const float pulloutSuccessRate = 0.8f;
        const float fetishPulloutSuccessModifier = 0.25f;

        public CompProperties_Menstruation Props;
        public Stage curStage = Stage.Follicular;
        public int curStageTicks = 0;   // Actual number of ticks equals this / cycleAcceleration
        private int tickInterval = -1;
        const int recalculateTickInterval = GenTicks.TickLongInterval;
        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,
            Infertile,
            Anestrus
        }

        public enum EstrusLevel
        {
            None,
            Concealed,
            Visible
        }

        public static readonly Dictionary<Stage, Texture2D> StageTexture = new Dictionary<Stage, Texture2D>()
        {
            { Stage.Follicular, TextureCache.FollicularTexture },
            { Stage.Ovulatory, TextureCache.OvulatoryTexture },
            { Stage.Luteal, TextureCache.LutealTexture },
            { Stage.Bleeding, 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 currentIntervalTicks = -1;   // Actual number of ticks equals this / cycleAcceleration
        protected float crampPain = -1;
        protected float fluidToLeak = 0;
        protected Need sexNeed = null;
        protected string customwombtex = null;
        protected string customvagtex = null;
        protected bool estrusflag = false;
        protected float? ovulationChanceCache = null;   // Dirtied every simulation
        protected float? implantationChanceCache = null;
        protected int? opcache = null;
        protected float antisperm = 0.0f;
        // RJW pregnancy, or Biotech pregnancy/labor/laborpushing
        protected Hediff pregnancy = null;

        protected int eggLifeSpanTicks = 2 * GenDate.TicksPerDay;
        protected EstrusLevel estrusLevel = EstrusLevel.Visible;
        protected float ovulationFactor = 1f;
        protected bool noBleeding = false;

        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)
        };

        protected int TickInterval
        {
            get
            {
                if (tickInterval <= 0)
                {
                    if (Pawn.IsAnimal()) tickInterval = Configurations.AnimalTickInterval;
                    else if (Pawn.IsColonist || Pawn.IsPrisonerOfColony || Pawn.IsSlaveOfColony) tickInterval = Configurations.ColonistTickInterval;
                    else tickInterval = Configurations.NonColonistTickInterval;
                    if (tickInterval < Configurations.TickIntervalMinimum) tickInterval = Configurations.TickIntervalMinimum;
                    else if (tickInterval > Configurations.TickIntervalMaximum) tickInterval = Configurations.TickIntervalMaximum;
                }
                return tickInterval;
            }
            set => tickInterval = value;
        }

        public float HoursBetweenSimulations => (float)TickInterval / GenDate.TicksPerHour;

        public Hediff 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.HasValue) return opcache.Value;
                const float yearsBeforeMenopause = 6.0f;
                opcache = (int)(RaceCyclesPerYear() *
                    AverageLitterSize() *
                    yearsBeforeMenopause *
                    (Pawn.RaceProps.lifeExpectancy / ThingDefOf.Human.race.lifeExpectancy));
                if (opcache <= 0) opcache = 1;
                return opcache.Value;
            }
        }

        // >= 1: Normal cycles
        // 1 - 0: Climacteric
        // <= 0: Menopause
        public float EggHealth
        {
            get
            {
                if (!Configurations.EnableMenopause || Props.infertile) return Mathf.Max(1.0f, ovarypower / OvaryPowerThreshold);
                else return (float)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 => cums?.Sum(cum => cum.Volume) ?? 0;
        }
        public float TotalFertCum
        {
            get => cums?.Where(cum => CumCanFertilize(cum)).Sum(cum => cum.FertVolume) ?? 0;
        }
        public float TotalCumPercent
        {
            get => 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 (Pawn.IsBreeder()) return 0.5f;

                return 1.0f;
            }
        }

        // I hate doing this, but it's the least bad option
        private bool calculatingOvulationChance = false;
        public bool CalculatingOvulationChance { get => calculatingOvulationChance; }

        private float CalculatedOvulationChance()
        {
            float ovulationChance = 1.0f;
            if (EggHealth <= 0.0f) return 0.0f;
            if (EggHealth < 1.0f / 3.0f) ovulationChance = 0.8f;
            if (ModsConfig.BiotechActive && xxx.is_human(Pawn))
            {
                if (Pawn.SterileGenes()) return 0.0f;
                if (!Pawn.RaceHasFertility()) return 0.0f;
                if (AndroidsCompatibility.IsAndroid(Pawn) && parent.def != Genital_Helper.archotech_vagina) return 0.0f;

                float ageFactor = 1.0f;
                StatDefOf.Fertility.GetStatPart<StatPart_FertilityByGenderAge>()?.TransformValue(StatRequest.For(Pawn), ref ageFactor);
                if (ageFactor <= 0.0f) return 0.0f;    // Too young or too old

                if (Pawn.IsBreeder()) ovulationChance *= 10.0f;
                try
                {
                    StatDefOf.Fertility.Worker.ClearCacheForThing(Pawn);    // No effect for now, but a future RW update might add caching for this
                    calculatingOvulationChance = true;
                    ovulationChance *= PawnCapacityUtility.CalculateCapacityLevel(Pawn.health.hediffSet, xxx.reproduction);
                }
                finally
                {
                    calculatingOvulationChance = false;
                    StatDefOf.Fertility.Worker.ClearCacheForThing(Pawn);
                }
            }
            return ovulationChance;
        }

        private float CalculatedImplantChance()
        {
            if (ModsConfig.BiotechActive && xxx.is_human(Pawn))
            {
                // Implant factor will be based solely on pawn age, plus any rollover from ovulation chance
                float factor = 1.0f;
                StatDefOf.Fertility.GetStatPart<StatPart_FertilityByGenderAge>()?.TransformValue(StatRequest.For(Pawn), ref factor);
                if (factor <= 0.0f) return 0.0f;
                if (OvulationChance > 1.0f) factor *= OvulationChance;
                return Props.baseImplantationChanceFactor * FertilityModifier * factor;
            }
            else
            {
                return Pawn.health.capacities.GetLevel(xxx.reproduction) * Props.baseImplantationChanceFactor * FertilityModifier * (Pawn.IsBreeder() ? 10.0f : 1.0f);
            }
        }

        public float OvulationChance
        {
            get
            {
                if (ovulationChanceCache == null) ovulationChanceCache = CalculatedOvulationChance();
                return ovulationChanceCache.Value;
            }
        }

        // Before configuration setting
        public float ImplantChance
        {
            get
            {
                if (implantationChanceCache == null) implantationChanceCache = CalculatedImplantChance();
                return implantationChanceCache.Value;
            }
        }

        public IEnumerable<string> GetCumsInfo
        {
            get
            {
                if (cums.NullOrEmpty()) yield return Translations.Info_noCum;
                else foreach (Cum cum in cums)
                        yield return string.Format("{0}: {1:0.##}ml", cum.notcum ? cum.notcumLabel : cum.pawn?.Label, 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.Infertile:
                        if (EggHealth <= 0f) return Translations.Stage_Menopause;
                        else return Translations.Stage_None;
                    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.Infertile:
                        if (EggHealth <= 0f) return Translations.Stage_Menopause_Desc;
                        else return Translations.Stage_None_Desc;
                    case Stage.Anestrus:
                        return Translations.Stage_Anestrus_Desc;
                    default:
                        return "";
                }
            }
        }

        public string WombTex
        {
            get => customwombtex ?? Props.wombTex;
            set => customwombtex = value;
        }
        public string VagTex
        {
            get => 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 EggFertilizedTime
        {
            get
            {
                if (eggs?.All(egg => !egg.fertilized) ?? true) return -1;
                return eggs.Max(egg => egg.ticksSinceFertilization);
            }
        }
        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 => !eggs.NullOrEmpty();
        }

        public int EggLifespanTicks
        {
            get => eggLifeSpanTicks;
        }

        public virtual bool IsDangerDay
        {
            get
            {
                if (Pawn.HasIUD()) return false;

                switch (curStage)
                {
                    case Stage.Follicular:
                        return curStageTicks > 0.7f * currentIntervalTicks;
                    case Stage.Ovulatory:
                        return true;
                    case Stage.Luteal:
                        return curStageTicks < EggLifespanTicks;
                    default:
                        return false;
                }
            }
        }
        public int GetNumOfEggs
        {
            get => eggs?.Count ?? 0;
        }
        public Color BloodColor
        {
            get
            {
                try
                {
                    return Utility.BloodDef(Pawn).graphicData.color;
                }
                catch (Exception ex)
                {
                    Log.WarningOnce($"Error when fetching blood color of {Pawn}: {ex}", Pawn.thingIDNumber + 657493519);
                    return Colors.blood;
                }
            }
        }

        public int CurStageIntervalTicks
        {
            get => currentIntervalTicks;
        }

        public float StageProgress
        {
            get
            {
                if (pregnancy == null) return Mathf.Clamp01((float)curStageTicks / currentIntervalTicks);
                bool is_discovered = false;
                switch (pregnancy)
                {
                    case Hediff_BasePregnancy rjw_preg:
                        is_discovered = rjw_preg.is_discovered;
                        break;
                    case Hediff_Pregnant vanilla_preg:
                        is_discovered = vanilla_preg.Visible;
                        break;
                    case Hediff_Labor _:
                    case Hediff_LaborPushing _:
                        return 1.0f;
                }

                if (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 * GenDate.HoursPerDay));

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

        public float StageProgessNextUpdate
        {
            get
            {
                if (pregnancy != null) return StageProgress;
                return Mathf.Clamp01((float)(curStageTicks + TickInterval * Configurations.CycleAcceleration) / currentIntervalTicks);
            }
        }

        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 curStageTicks, "curStageTicks", curStageTicks, true);
            Scribe_Values.Look(ref cycleSpeed, "cycleSpeed", cycleSpeed, true);
            Scribe_Values.Look(ref cycleVariability, "cycleVariability", cycleVariability, true);
            Scribe_Values.Look(ref currentIntervalTicks, "currentIntervalTicks", currentIntervalTicks, true);
            Scribe_Values.Look(ref crampPain, "crampPain", crampPain, true);
            Scribe_Values.Look(ref fluidToLeak, "fluidToLeak", 0);
            Scribe_Values.Look(ref ovarypower, "ovarypower", ovarypower, true);
            Scribe_Values.Look(ref eggstack, "eggstack", 0);
            Scribe_Values.Look(ref estrusflag, "estrusflag", false);
            Scribe_Values.Look(ref DoCleanWomb, "DoCleanWomb", false);
            Scribe_References.Look(ref pregnancy, "pregnancy");
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
            {
                Initialize();
                // Biotech labor errors if the mother is also the father, so don't let that happen
                if (pregnancy is HediffWithParents vanillaPreg && vanillaPreg.Mother == vanillaPreg.Father && vanillaPreg.Mother != null)
                {
                    Log.Warning($"Pregnancy of {Pawn} has same mother and father, setting father to null");
                    vanillaPreg.SetParents(vanillaPreg.Mother, null, vanillaPreg.geneSet);
                }
            }
        }

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

        public void Notify_UpdatedGenes()
        {
            eggLifeSpanTicks = Props.eggLifespanDays * GenDate.TicksPerDay;
            estrusLevel = Props.concealedEstrus ? EstrusLevel.Concealed : EstrusLevel.Visible;
            ovulationFactor = 1f;
            noBleeding = false;
            opcache = null;

            if (Pawn.genes == null || !ModsConfig.BiotechActive) return;
            foreach (MenstruationModExtension extension in Pawn.genes.GenesListForReading.Where(gene => gene.Active).Select(gene => gene.def.GetModExtension<MenstruationModExtension>()).Where(ext => ext != null))
            {
                eggLifeSpanTicks = (int)(eggLifeSpanTicks * extension.eggLifeTimeFactor);
                if (extension.alwaysEstrus) estrusLevel = EstrusLevel.Visible;
                else if (extension.neverEstrus) estrusLevel = EstrusLevel.None;
                ovulationFactor *= extension.ovulationFactor;
                if (extension.noBleeding) noBleeding = true;
            }
            if (eggLifeSpanTicks < 0) eggLifeSpanTicks = 0;
            if (ovulationFactor < 0f) ovulationFactor = 0f;
        }

        public bool ShouldSimulate()
        {
            if (!Pawn.ShouldCycle()) return false;
            if (Pawn.SpawnedOrAnyParentSpawned || Pawn.IsCaravanMember() || PawnUtility.IsTravelingInTransportPodWorldObject(Pawn)) return true;

            return false;
        }

        public bool ShouldBeInfertile()
        {
            if (pregnancy != null) return false;
            if (ImplantChance <= 0.0f) return true;
            // Give the last egg ovulated a chance to implant
            if (curStage == Stage.Luteal || curStage == Stage.Bleeding || curStage == Stage.Recover) return false;
            if (EggHealth <= 0.0f) 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 (Pawn.IsHashIntervalTick(recalculateTickInterval)) TickInterval = -1;  // Every so often, force TickInterval to be recalculated in case the pawn's status changed.
                if (!Pawn.IsHashIntervalTick(TickInterval)) return;

                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 (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");
                    GoNextStage(Stage.Pregnant);
                }

                BeforeSimulator();

                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.Infertile:
                        InfertileAction();
                        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;
            }
            switch (pregnancy)
            {
                case null:
                case Hediff_MechanoidPregnancy _:
                    break;
                case Hediff_BasePregnancy rjw_preg:
                    rjw_preg.Miscarry();
                    break;
                case Hediff_Pregnant vanilla_preg:
                    Pawn.health.RemoveHediff(vanilla_preg);
                    break;
            }

            base.CompPostPostRemoved();
        }

        public override string CompTipStringExtra
        {
            get
            {
                if (Pawn.Dead || !Pawn.ShouldCycle()) return null;
                StringBuilder tip = new StringBuilder();
                tip.Append(Translations.Dialog_WombInfo01);
                tip.Append(": ");
                tip.Append(GetCurStageLabel);
                string fertInfo = GetFertilizingInfo;
                if (CurrentVisibleStage == Stage.Luteal && fertInfo.Length > 0)
                {
                    tip.AppendLine();
                    tip.Append(fertInfo);
                }
                return tip.ToString();
            }
        }

        protected virtual int TicksToNextStage()
        {
            return Math.Max(0, (currentIntervalTicks - curStageTicks) / Configurations.CycleAcceleration);
        }

        public override string CompDebugString()
        {
            if (Pawn.Dead ||
                !Pawn.ShouldCycle() ||
                curStage == Stage.None ||
                curStage == Stage.Infertile ||
                curStage == Stage.Pregnant) return null;
            StringBuilder debugString = new StringBuilder();
            debugString.Append($"Time to next state: ");
            debugString.Append(GenDate.ToStringTicksToPeriod(TicksToNextStage()));
            return debugString.ToString();
        }

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

        /// <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="cummer"></param>
        /// <param name="volume"></param>
        /// <param name="fertility"></param>
        /// <param name="precum"></param>
        public void CumIn(Pawn cummer, float volume, float fertility = 1.0f, bool precum = false)
        {
            if (volume <= 0)
            {
                if (!precum) Log.Warning($"{cummer} trying to cum inside of {Pawn} with 0 fluid");
                return;
            }
            if (!precum && fertility > 0 && IsDangerDay && cummer.relations.GetPregnancyApproachForPartner(Pawn) == PregnancyApproach.AvoidPregnancy)
            {
                float successChance = pulloutSuccessRate;
                if (cummer.HasImpregnationFetish()) successChance *= fetishPulloutSuccessModifier;
                if (Pawn.HasImpregnationFetish()) successChance *= fetishPulloutSuccessModifier;
                if (Rand.Chance(successChance)) return;
            }
            if (Pawn.HasIUD()) 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(cummer) ?? false)
                        {
                            cum.MergeWithCum(volume, fertility);
                            merged = true;
                        }
                        cum.DismishForce(cumoutrate);
                    }
                if (!merged) cums.Add(new Cum(cummer, volume * (1 - cumoutrate), fertility));
            }
            else
            {

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

            if (!precum)
            {
                Pawn.records.AddTo(VariousDefOf.AmountofCreampied, volume);
                AfterCumIn(cummer);
                AfterFluidIn(cumd);
            }
        }

        /// <summary>
        /// Inject pawn's fluid into womb
        /// </summary>
        /// <param name="cummer"></param>
        /// <param name="volume"></param>
        /// <param name="notcumlabel"></param>
        /// <param name="decayresist"></param>
        /// <param name="filthdef"></param>
        public void CumIn(Pawn cummer, float volume, string notcumlabel, float decayresist = 0, ThingDef filthdef = null)
        {
            if (volume <= 0)
            {
                Log.Warning($"{cummer} trying to cum {notcumlabel} inside of {Pawn} with 0 fluid");
                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(cummer) ?? false) && cum.notcumLabel.Equals(notcumlabel))
                        {
                            cum.MergeWithFluid(volume, decayresist, filthdef);
                            merged = true;
                        }
                        cum.DismishForce(cumoutrate);
                    }
                if (!merged) cums.Add(new Cum(cummer, volume * (1 - cumoutrate), notcumlabel, decayresist, filthdef));
            }
            else
            {

                bool merged = false;
                if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
                    {
                        if (cum.notcum && (cum.pawn?.Equals(cummer) ?? false) && cum.notcumLabel.Equals(notcumlabel))
                        {
                            cum.MergeWithFluid(volume, decayresist, filthdef);
                            merged = true;
                        }
                    }
                if (!merged) cums.Add(new Cum(cummer, 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.HasIUD()) antisperm = 0.70f + asafactor;
            else antisperm = 0.0f + asafactor;

            absorber = Pawn.apparel?.WornApparel.OfType<Absorber>().FirstOrDefault();
            if (absorber != null)
            {
                absorber.WearEffect(TickInterval);
                if (absorber.dirty && absorber.EffectAfterDirty) absorber.DirtyEffect(TickInterval);
            }
        }

        /// <summary>
        /// For natural leaking
        /// </summary>
        protected virtual void AfterCumOut(float amount, List<string> filthlabels)
        {
            fluidToLeak += amount;
            if (fluidToLeak > fluidLeakThreshold) Pawn.needs?.mood?.thoughts?.memories?.TryGainMemory(VariousDefOf.LeakingFluids);
            for (; fluidToLeak > fluidLeakThreshold; fluidToLeak -= fluidLeakThreshold)
            {
                if (cums.Count > 1) MakeCumFilthMixture(fluidLeakThreshold, filthlabels);
                else if (cums.Count == 1) MakeCumFilth(cums.First(), fluidLeakThreshold);
            }
        }

        /// <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>(cums.Count);
            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;
            foreach (Cum cum in cums)
            {
                if (Rand.Chance(HoursBetweenSimulations)) cum.CumEffects(Pawn);
                float vd = cum.DismishNatural(leakfactor, this, antisperm);
                cum.MakeThinner((float)Configurations.CycleAcceleration * HoursBetweenSimulations);
                totalleak += AbsorbCum(vd, absorber);
                string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
                filthlabels.Add(tmp.Replace(" x1", ""));
            }
            AfterCumOut(totalleak, filthlabels);
            cums.RemoveAll(cum => cum.ShouldRemove());
            if (cums.NullOrEmpty()) fluidToLeak = 0;
            cumd = TotalCumPercent - cumd;
            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>(cums.Count);
            float outcum = 0;
            float cumd = TotalCumPercent;
            HashSet<Cum> removecums = new HashSet<Cum>(cums.Count);
            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);
            cums.RemoveAll(cum => removecums.Contains(cum));
            if (cums.NullOrEmpty()) fluidToLeak = 0;
            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>(cums.Count);
            HashSet<Cum> removecums = new HashSet<Cum>(cums.Count);
            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;
            }
            cums.RemoveAll(cum => removecums.Contains(cum));
            if (cums.NullOrEmpty()) fluidToLeak = 0;
            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();
        }

        public void RemoveAllEggs()
        {
            eggs.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 (cums == null) cums = new List<Cum>();
            if (eggs == null) eggs = new List<Egg>();

            PreInitialize();

            if (Props.infertile)
            {
                curStage = Stage.None;
                loaded = true;
                initError = false;
                return;
            }

            if (cycleSpeed < 0f) cycleSpeed = Utility.RandGaussianLike(0.8f, 1.2f);
            if (cycleVariability < 0f) cycleVariability = MenstruationUtility.RandomVariabilityPercent();

            InitOvary();

            if (currentIntervalTicks < 0)
            {
                if (ShouldBeInfertile()) curStage = Stage.Infertile;
                else if (!IsBreedingSeason()) curStage = Stage.Anestrus;
                else curStage = RandomStage();
                if (curStage == Stage.Follicular)
                    currentIntervalTicks = PeriodRandomizer(Stage.Follicular) - PeriodRandomizer(Stage.Bleeding);
                else
                    currentIntervalTicks = PeriodRandomizer(curStage);
                if (currentIntervalTicks < 0) currentIntervalTicks = 0;
                else if (currentIntervalTicks < curStageTicks) curStageTicks = currentIntervalTicks;
            }
            if (crampPain < 0) crampPain = PainRandomizer();
            PostInitialize();

            TakeLoosePregnancy();

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

        protected virtual void PreInitialize()
        {
            Notify_UpdatedGenes();
        }

        protected virtual void PostInitialize()
        {
        }

        protected float AverageLitterSize()
        {
            float avglittersize;
            try
            {
                avglittersize = Mathf.Max(Rand.ByCurveAverage(Pawn.RaceProps.litterSizeCurve), 1.0f);
            }
            catch (NullReferenceException)
            {
                avglittersize = 1.0f;
            }
            avglittersize *= ovulationFactor;
            return avglittersize;
        }

        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 = AverageLitterSize();

            float fertStartAge = Pawn.RaceProps.lifeStageAges?.Find(stage => stage.def.reproductive)?.minAge ?? 0.0f;
            float fertEndAge = Pawn.RaceProps.lifeExpectancy * (Pawn.IsAnimal() ? 0.96f : 0.58f);   // RJW default fertility_endage for animal and 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.Infertile;
                }
            }
        }

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

        protected virtual void BeforeSimulator()
        {
            ovulationChanceCache = null;
            implantationChanceCache = null;
            CumOut();
        }

        protected virtual void AfterSimulator()
        {
            if (ShouldBeInfertile())
            {
                eggs.Clear();
                GoNextStage(Stage.Infertile);
            }
            if (EggHealth < 1f)
            {
                if (sexNeed == null) sexNeed = Pawn.needs.TryGetNeed(VariousDefOf.SexNeed);
                if (sexNeed == null) return;
                if (sexNeed.CurLevel < 0.5f) sexNeed.CurLevel += 0.01f * HoursBetweenSimulations / Math.Max(1, Pawn.GetMenstruationComps().Count());
            }
        }

        protected virtual bool ShouldBeInEstrus()
        {
            if (!loaded)
                Initialize();
            switch (curStage)
            {
                case Stage.Follicular:
                    return curStageTicks > currentIntervalTicks - Props.estrusDaysBeforeOvulation * GenDate.TicksPerDay;
                case Stage.Ovulatory:
                    return true;
                case Stage.Luteal:
                    return curStageTicks < EggLifespanTicks;
                default:
                    return false;
            }
        }

        public EstrusLevel GetEstrusLevel()
        {
            if (!ShouldBeInEstrus()) return EstrusLevel.None;
            else return estrusLevel;
        }

        public void SetEstrus()
        {
            if (estrusLevel == EstrusLevel.None) return;
            Hediff hediff = HediffMaker.MakeHediff(estrusLevel == EstrusLevel.Concealed ? 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;
            }
        }

        public bool CumCanFertilize(Cum cum)
        {
            return !cum.notcum &&
                cum.FertVolume > 0 &&
                !(cum.pawn?.Destroyed ?? true) &&
                (RJWPregnancySettings.bestial_pregnancy_enabled || xxx.is_animal(Pawn) == xxx.is_animal(cum.pawn));
        }

        protected Pawn Fertilize()
        {
            if (cums == null) return null;
            List<Cum> eligibleCum = cums.FindAll(cum => CumCanFertilize(cum));
            if (eligibleCum.Empty()) return null;

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

            //float fertFailChancePerHour = Mathf.Pow(1.0f - Configurations.FertilizeChance, totalFertPower * Props.basefertilizationChanceFactor);
            //float fertFailChancePerInterval = Mathf.Pow(fertFailChancePerHour, (float)TickInterval / GenDate.TicksPerHour);
            float fertFailChancePerInterval = Mathf.Pow(1.0f - Configurations.FertilizeChance, totalFertPower * Props.basefertilizationChanceFactor * HoursBetweenSimulations);

            if (Rand.Chance(fertFailChancePerInterval)) 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;

            HashSet<Egg> deadeggs = new HashSet<Egg>(eggs.Count);
            bool pregnant = false;
            foreach (Egg egg in eggs)
            {
                if (!egg.fertilized ||
                    egg.ticksSinceFertilization < minImplantAgeHours * GenDate.TicksPerHour ||
                    egg.ageTicks < Math.Min(Props.lutealIntervalDays * GenDate.TicksPerDay / 2, maxImplantDelayHours * GenDate.TicksPerHour))
                    continue;
                else if (egg.fertilizer == null || egg.fertilizer.Destroyed)
                {
                    if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to null or destroyed father");
                    deadeggs.Add(egg);
                    continue;
                }
                else if (Pawn.health.hediffSet.GetFirstHediff<Hediff_InsectEgg>() != null || 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 (ModsConfig.BiotechActive && !Find.Storyteller.difficulty.ChildrenAllowed)
                {
                    if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to children being disabled");
                    deadeggs.Add(egg);
                    continue;
                }
                else if (Rand.Chance(Configurations.ImplantationChance * ImplantChance * InterspeciesImplantFactor(egg.fertilizer)))
                {
                    try
                    {
                        if (Configurations.Debug) Log.Message($"Implanting fertilized egg of {Pawn} into {parent}, father {egg.fertilizer}");
                        if (pregnancy == null)
                        {
                            Configurations.PregnancyType usePregnancy = xxx.is_human(Pawn) ? Configurations.PregnancySource : Configurations.PregnancyType.MultiplePregnancy;
                            switch (usePregnancy)
                            {
                                case Configurations.PregnancyType.BaseRJW:

                                    if (Configurations.Debug) Log.Message($"Creating new base RJW pregnancy");
                                    PregnancyHelper.AddPregnancyHediff(Pawn, egg.fertilizer);
                                    // I hate having to do this, but it gets the newest pregnancy
                                    List<Hediff_BasePregnancy> pregnancies = new List<Hediff_BasePregnancy>();
                                    Pawn.health.hediffSet.GetHediffs(ref pregnancies);
                                    pregnancy = pregnancies.MaxBy(hediff => hediff.loadID);
                                    pregnant = true;
                                    break;

                                case Configurations.PregnancyType.MultiplePregnancy:
                                    if (Configurations.Debug) Log.Message($"Creating new menstruation pregnancy");
                                    pregnancy = Hediff_BasePregnancy.Create<Hediff_MultiplePregnancy>(Pawn, egg.fertilizer);
                                    pregnant = true;
                                    deadeggs.Add(egg);
                                    break;

                                case Configurations.PregnancyType.Biotech:
                                    if (Configurations.Debug) Log.Message($"Creating new biotech pregnancy");
                                    pregnancy = HediffMaker.MakeHediff(HediffDefOf.PregnantHuman, Pawn);
                                    if (Configurations.EnableBiotechTwins)
                                        pregnancy.TryGetComp<HediffComp_PregeneratedBabies>().AddNewBaby(Pawn, egg.fertilizer);
                                    Pawn father = egg.fertilizer;
                                    if (Pawn == father) father = null;
                                    ((Hediff_Pregnant)pregnancy).SetParents(Pawn, father, PregnancyUtility.GetInheritedGeneSet(father, Pawn));
                                    Pawn.health.AddHediff(pregnancy);
                                    pregnant = true;
                                    deadeggs.Add(egg);
                                    break;
                            }
                            if (pregnancy is Hediff_BasePregnancy rjw_preg)
                            {
                                // TODO: advance biotech pregnancy
                                rjw_preg.p_start_tick -= egg.ticksSinceFertilization / Configurations.CycleAcceleration;
                                rjw_preg.p_end_tick -= egg.ticksSinceFertilization / Configurations.CycleAcceleration;
                            }
                        }
                        else
                        {
                            switch (pregnancy)
                            {
                                case Hediff_Pregnant vanillaPreg:   // Not going to do the labor ones
                                    if (!Configurations.EnableBiotechTwins || !Configurations.EnableHeteroOvularTwins) goto default;
                                    if (Configurations.Debug) Log.Message($"Adding to existing Biotech pregnancy {vanillaPreg.GetUniqueLoadID()}");
                                    HediffComp_PregeneratedBabies comp = vanillaPreg.TryGetComp<HediffComp_PregeneratedBabies>();
                                    if (comp == null) Log.Warning($"Trying to add Biotech egg to {Pawn}'s pregnancy without a pregenerated baby comp: {vanillaPreg.GetUniqueLoadID()}");
                                    else
                                        comp.AddNewBaby(Pawn, egg.fertilizer);
                                    pregnant = true;
                                    deadeggs.Add(egg);
                                    break;
                                case Hediff_MultiplePregnancy multiPreg:
                                    if (!Configurations.EnableHeteroOvularTwins) goto default;
                                    if (Configurations.Debug) Log.Message($"Adding to existing pregnancy {multiPreg.GetUniqueLoadID()}");
                                    multiPreg.AddNewBaby(Pawn, egg.fertilizer);
                                    pregnant = true;
                                    deadeggs.Add(egg);
                                    break;
                                case Hediff_BasePregnancy _:
                                default:
                                    if (Configurations.Debug) Log.Message($"Not adding to existing pregnancy {pregnancy.GetUniqueLoadID()}");
                                    pregnant = true;
                                    deadeggs.Add(egg);
                                    break;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error($"Error creating pregnancy in {Pawn}'s womb, father {egg.fertilizer}: {ex}");
                        TakeLoosePregnancy();
                        deadeggs.Add(egg);
                    }
                }
                else
                {
                    if (Configurations.Debug)
                    {
                        float interspeciesFactor = InterspeciesImplantFactor(egg.fertilizer);
                        float implantChance = Configurations.ImplantationChance * ImplantChance * interspeciesFactor;
                        Log.Message($"Fertilized egg of {Pawn} failed to implant (chance {implantChance.ToStringPercent()}, " +
                            ((!Mathf.Approximately(interspeciesFactor, 1.0f)) ? $"interspecies factor {interspeciesFactor.ToStringPercent()}, " : "") +
                            $"father {egg.fertilizer})");
                    }
                    deadeggs.Add(egg);
                }
            }

            bool clearAllEggs = true;
            switch (Configurations.PregnancySource)
            {
                case Configurations.PregnancyType.BaseRJW:
                    clearAllEggs = true;
                    break;
                case Configurations.PregnancyType.MultiplePregnancy:
                    clearAllEggs = !Configurations.EnableHeteroOvularTwins;
                    break;
                case Configurations.PregnancyType.Biotech:
                    clearAllEggs = !(Configurations.EnableBiotechTwins && Configurations.EnableHeteroOvularTwins);
                    break;
            }

            if (pregnant && clearAllEggs)
                eggs.Clear();
            else
                eggs.RemoveAll(egg => deadeggs.Contains(egg));
            return pregnant;
        }

        protected void BleedOut()
        {
            // ~1.5 per hour times acceleration
            float bledAmount = Utility.VariationRange(0.03f * Configurations.BleedingAmount * Configurations.CycleAcceleration * HoursBetweenSimulations, 0.5f);
            CumIn(Pawn, bledAmount, Translations.Menstrual_Blood, -5.0f, Utility.BloodDef(Pawn));
            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.MapHeld == null) return;
            if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.PositionHeld, Pawn.MapHeld, 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;
            absorber.CheckDirty();
            return 0;
        }

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

        protected void EggDecay()
        {
            HashSet<Egg> deadeggs = new HashSet<Egg>(eggs.Count);
            foreach (Egg egg in eggs)
            {
                egg.ageTicks += TickInterval * Configurations.CycleAcceleration;
                if (egg.fertilized) egg.ticksSinceFertilization += TickInterval * Configurations.CycleAcceleration;
                else
                {
                    egg.lifeSpanTicks -= TickInterval * Configurations.CycleAcceleration;
                    if (egg.lifeSpanTicks < 0) deadeggs.Add(egg);
                }
            }
            eggs.RemoveAll(egg => deadeggs.Contains(egg));
        }

        protected void AddCrampPain()
        {
            Hediff hediff = HediffMaker.MakeHediff(VariousDefOf.Hediff_MenstrualCramp, Pawn);
            hediff.Severity = Utility.VariationRange(crampPain, 0.1f);
            HediffCompProperties_SeverityPerDay Prop = (HediffCompProperties_SeverityPerDay)hediff.TryGetComp<HediffComp_SeverityPerDay>().props;
            Prop.severityPerDay = -hediff.Severity / (currentIntervalTicks / GenDate.TicksPerDay) * Configurations.CycleAcceleration;
            Pawn.health.AddHediff(hediff, parent.Part);
        }

        protected void AdvanceStageTime()
        {
            curStageTicks += TickInterval * Configurations.CycleAcceleration;
        }

        protected virtual void FollicularAction()
        {
            if (!IsBreedingSeason())
            {
                estrusflag = false;
                GoNextStage(Stage.Anestrus);
                return;
            }
            else if (curStageTicks >= currentIntervalTicks)
            {
                GoOvulatoryStage();
            }
            else
            {
                AdvanceStageTime();
                if (!estrusflag && curStageTicks > currentIntervalTicks - Props.estrusDaysBeforeOvulation * GenDate.TicksPerDay)
                {
                    estrusflag = true;
                    SetEstrus();
                }
            }
        }

        protected virtual void OvulatoryAction()
        {
            if (curStageTicks < currentIntervalTicks)
            {
                AdvanceStageTime();
                return;
            }
            estrusflag = false;
            float eggnum;
            try
            {
                eggnum = Math.Max(Rand.ByCurve(Pawn.RaceProps.litterSizeCurve), 1f);
            }
            catch (NullReferenceException)
            {
                eggnum = 1f;
            }
            catch (ArgumentException e)
            {
                Log.WarningOnce($"Invalid litterSizeCurve for {Pawn.def}: {e}", 642201874 + Pawn.thingIDNumber);
                eggnum = 1f;
            }
            eggnum *= ovulationFactor;
            int toOvulate = Math.Max(1, (int)eggnum + eggstack);

            int ovulated = 0;
            for (int i = 0; i < toOvulate; i++)
                if (i < eggstack || Rand.Chance(OvulationChance))   // eggstack comes from drugs and are guaranteed ovulated
                {
                    eggs.Add(new Egg((int)(EggLifespanTicks / CycleFactor)));
                    ++ovulated;
                }
            ovarypower -= ovulated;
            eggstack = 0;
            if (Configurations.Debug && ovulated < toOvulate)
                Log.Message($"{Pawn} ovulated {ovulated}/{toOvulate} eggs ({OvulationChance.ToStringPercent()} chance)");

            GoNextStage(Stage.Luteal);
        }

        protected virtual void LutealAction()
        {
            if (curStageTicks >= currentIntervalTicks)
            {
                eggs.Clear();
                if (EggHealth < 1f / 4f || (EggHealth < 1f / 3f && Rand.Chance(0.3f))) //skips bleeding
                {
                    GoNextStage(Stage.Follicular);
                }
                else
                {
                    GoFollicularOrBleeding();
                }
            }
            else if (!eggs.NullOrEmpty())
            {
                FertilizationCheck();
                EggDecay();
                if (Implant())
                {
                    GoNextStage(Stage.Pregnant);
                }
                else
                {
                    AdvanceStageTime();
                }
            }
            else
            {
                AdvanceStageTime();
            }

        }

        protected virtual void BleedingAction()
        {
            if (curStageTicks >= currentIntervalTicks || noBleeding)
            {
                Hediff hediff = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_MenstrualCramp);
                if (hediff != null && !Pawn.GetMenstruationComps().Any(comp => comp != this && comp.curStage == Stage.Bleeding)) Pawn.health.RemoveHediff(hediff);
                int totalFollicularTicks = PeriodRandomizer(Stage.Follicular);  // The total amount of time for both bleeding and follicular
                if (totalFollicularTicks <= currentIntervalTicks)   // We've bled for so long that we completely missed the follicular phase
                    GoOvulatoryStage();
                else
                {
                    GoNextStage(Stage.Follicular, totalFollicularTicks - currentIntervalTicks); // I.e., the remaining follicular time equals the total minus the bleeding time elapsed
                }
            }
            else
            {
                if (curStageTicks < currentIntervalTicks / 4) BleedOut();
                AdvanceStageTime();
            }
        }

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

            if (Pregnancy != null)
                curStageTicks += TickInterval;
            else
                GoNextStage(Stage.Recover);
        }

        protected virtual void RecoverAction()
        {
            if (curStageTicks >= currentIntervalTicks)
            {
                if (!IsBreedingSeason())
                {
                    GoNextStage(Stage.Anestrus);
                }
                else
                {
                    GoNextStage(Stage.Follicular);
                }
            }
            else
            {
                AdvanceStageTime();
            }
        }

        protected virtual void InfertileAction()
        {
            if (ShouldBeInfertile())
                return;
            else if (IsBreedingSeason())
                GoNextStage(Stage.Follicular);
            else
                GoNextStage(Stage.Anestrus);
        }

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

        protected virtual void ThoughtCumInside(Pawn cummer)
        {
            MemoryThoughtHandler cummerMemories = cummer?.needs?.mood?.thoughts.memories;
            MemoryThoughtHandler pawnMemories = Pawn?.needs?.mood?.thoughts.memories;

            if (cummerMemories == null || pawnMemories == null) return;

            if (cummer.IsProPregnancy(out Precept preceptm) || (cummer.IsTeratophile() != (Pawn.GetStatValue(StatDefOf.PawnBeauty) >= 0)))
            {
                if (cummer.relations.OpinionOf(Pawn) <= -5)
                    cummerMemories.TryGainMemory(VariousDefOf.HaterCameInsideM, Pawn);
                else if (preceptm != null)
                    cummerMemories.TryGainMemory(VariousDefOf.CameInsideMIdeo, Pawn, preceptm);
                else
                    cummerMemories.TryGainMemory(VariousDefOf.CameInsideM, Pawn);
            }

            if (IsDangerDay)
            {
                if (Pawn.IsProPregnancy(out Precept preceptf))
                {
                    if (preceptf != null)
                    {
                        if (Pawn.relations.OpinionOf(cummer) <= -5)
                            pawnMemories.TryGainMemory(VariousDefOf.HaterCameInsideFIdeo, cummer, preceptf);
                        else
                            pawnMemories.TryGainMemory(VariousDefOf.CameInsideFIdeo, cummer, preceptf);
                    }
                    else pawnMemories.TryGainMemory(VariousDefOf.CameInsideFFetish, cummer);
                }
                else if (Pawn.relations.OpinionOf(cummer) <= -5)
                    pawnMemories.TryGainMemory(VariousDefOf.HaterCameInsideF, cummer);
                else if (Pawn.IsInEstrus() && Pawn.relations.OpinionOf(cummer) < RJWHookupSettings.MinimumRelationshipToHookup)
                    pawnMemories.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) pawnMemories.TryGainMemory(VariousDefOf.CameInsideFLowFert, cummer);
                    else pawnMemories.TryGainMemory(VariousDefOf.CameInsideF, cummer);
                }
            }
            else
            {
                if (Pawn.IsProPregnancy(out Precept _))
                    pawnMemories.TryGainMemory(VariousDefOf.CameInsideFFetishSafe, cummer);
                else if (Pawn.relations.OpinionOf(cummer) <= -5)
                    pawnMemories.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 });
        }

        public void GoNextStage(Stage nextstage, int? stageTicks = null)
        {
            curStageTicks = 0;
            currentIntervalTicks = stageTicks ?? PeriodRandomizer(nextstage);
            curStage = nextstage;
        }

        protected virtual void GoOvulatoryStage()
        {
            GoNextStage(Stage.Ovulatory);
        }

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

        protected int PeriodRandomizer(Stage stage)
        {
            float CycleSkew(float factor) => 1 + (cycleSpeed - 1) * factor;

            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:
                    return (int)(Utility.VariationRange(Props.follicularIntervalDays * GenDate.TicksPerDay, cycleVariability * 1.5f * variabilityFactor) / CycleSkew(1.5f));
                case Stage.Ovulatory:
                    return Props.ovulationIntervalHours * GenDate.TicksPerHour; // No variability for now
                case Stage.Luteal:
                    return (int)(Utility.VariationRange(Props.lutealIntervalDays * GenDate.TicksPerDay, cycleVariability * 0.5f * variabilityFactor) / CycleSkew(0.5f));
                case Stage.Bleeding:
                    return (int)(Utility.VariationRange(Props.bleedingIntervalDays * GenDate.TicksPerDay, cycleVariability * 0.5f * variabilityFactor) / CycleSkew(0.5f));
                case Stage.Recover:
                    return (int)Utility.VariationRange(Props.recoveryIntervalDays * GenDate.TicksPerDay, 0.05f);
                case Stage.Pregnant:
                    return (int)(MenstruationUtility.GestationHours(pregnancy) * GenDate.TicksPerHour);
                default:
                    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 virtual float RandomOvulationChance => (float)Props.ovulationIntervalHours / GenDate.HoursPerDay;

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

            switch (stage)
            {
                case Stage.Follicular:
                    curStageTicks = Rand.Range(0, (Props.follicularIntervalDays - Props.bleedingIntervalDays) * GenDate.TicksPerDay);
                    break;
                case Stage.Ovulatory:
                    curStageTicks = Rand.Range(0, Props.ovulationIntervalHours * GenDate.TicksPerHour);
                    break;
                case Stage.Luteal:
                    curStageTicks = Rand.Range(0, Props.lutealIntervalDays * GenDate.TicksPerDay);
                    break;
                case Stage.Bleeding:
                    curStageTicks = Rand.Range(0, Props.bleedingIntervalDays * GenDate.TicksPerDay);
                    break;
            }
            return stage;
        }

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

            IEnumerable<Hediff> pregnancies = Pawn.health.hediffSet.hediffs.Where(hediff =>
                hediff is Hediff_BasePregnancy ||
                hediff is Hediff_Pregnant ||
                hediff is Hediff_Labor ||
                hediff is Hediff_LaborPushing);

            pregnancy =
                pregnancies.Except(
                    Pawn.GetMenstruationComps().Select(comp => comp.pregnancy).Where(preg => preg != null)
                    ).FirstOrDefault();
            if (pregnancy != null)
                GoNextStage(Stage.Pregnant);
        }

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

        public int EggsRestoredPerBiosculptor(float yearsWorth)
        {
            return Math.Max(1, (int)(RaceCyclesPerYear() * yearsWorth * AverageLitterSize()));
        }

        public void RestoreEggs(float yearsWorth)
        {
            ovarypower += EggsRestoredPerBiosculptor(yearsWorth);
        }

        public class Egg : IExposable
        {
            public bool fertilized;
            public int lifeSpanTicks;   // Actual ticks scaled by cycleAcceleration
            public Pawn fertilizer;
            public int ageTicks;
            public int ticksSinceFertilization = 0;

            public Egg()
            {
                fertilized = false;
                lifeSpanTicks = (int)(VariousDefOf.HumanVaginaCompProperties.eggLifespanDays * GenDate.TicksPerDay * Configurations.EggLifespanMultiplier);
                fertilizer = null;
                ageTicks = 0;
            }

            public Egg(int lifespanticks)
            {
                fertilized = false;
                lifeSpanTicks = (int)(lifespanticks * Configurations.EggLifespanMultiplier);
                fertilizer = null;
                ageTicks = 0;
            }

            public void ExposeData()
            {
                Scribe_References.Look(ref fertilizer, "fertilizer", true);
                Scribe_Values.Look(ref fertilized, "fertilized", false);
                Scribe_Values.Look(ref lifeSpanTicks, "lifeSpanTicks", lifeSpanTicks, true);
                Scribe_Values.Look(ref ageTicks, "ageTicks", ageTicks, true);
                Scribe_Values.Look(ref ticksSinceFertilization, "ticksSinceFertilization", 0);
            }
        }
    }

    public class HediffComp_Anus : HediffComp
    {
        public CompProperties_Anus Props => (CompProperties_Anus)props;
    }
}


code:
using RimWorld;
using Verse;

namespace RJW_Menstruation
{
    public class CompProperties_PeriodicOvulator : CompProperties_Menstruation
    {
        public FloatRange cycleIntervalDays; // From the start of one cycle to the start of the next

        public CompProperties_PeriodicOvulator()
        {
            compClass = typeof(HediffComp_PeriodicOvulator);
        }
    }

    public class HediffComp_PeriodicOvulator : HediffComp_Menstruation
    {
        public int ticksToNextCycle = -100000;
        public int averageCycleIntervalTicks = -1;

        public new CompProperties_PeriodicOvulator Props;

        protected override void PreInitialize()
        {
            base.PreInitialize();
            Props = (CompProperties_PeriodicOvulator)props;
        }

        protected override void PostInitialize()
        {
            base.PostInitialize();
            if (averageCycleIntervalTicks < 0)
            {
                averageCycleIntervalTicks = (int)(Props.cycleIntervalDays.RandomInRange * GenDate.TicksPerDay / cycleSpeed);
                if (ticksToNextCycle < -50000)
                    ticksToNextCycle = Rand.Range(0, averageCycleIntervalTicks);
                // Make the cutoff halfway into cycle, just to be sure there isn't a double-cycle the first time
                if ((curStage == Stage.Follicular || curStage == Stage.Ovulatory || curStage == Stage.Luteal || curStage == Stage.Bleeding)
                    && (averageCycleIntervalTicks - ticksToNextCycle) / 2 >= GenDate.TicksPerDay * (Props.follicularIntervalDays + Props.lutealIntervalDays) / cycleSpeed)
                    GoNextStage(Stage.Anestrus);
            }
        }

        public override string GetCurStageDesc
        {
            get
            {
                switch (CurrentVisibleStage)
                {
                    case Stage.Anestrus:
                        if (IsBreedingSeason()) return Translations.Stage_Anestrus_Periodic_Desc;
                        else goto default;
                    default:
                        return base.GetCurStageDesc;
                }
            }
        }

        protected override float RaceCyclesPerYear()
        {
            // Don't bother trying to work seasonal breeding into the math
            // Base it off of the shortest cycle interval of the vagina
            return GenDate.DaysPerYear / (Props.cycleIntervalDays.TrueMin / Configurations.CycleAccelerationDefault);
        }

        protected override void BeforeSimulator()
        {
            base.BeforeSimulator();
            if (ticksToNextCycle > 0) ticksToNextCycle -= TickInterval * Configurations.CycleAcceleration;
        }

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

        protected override int TicksToNextStage()
        {
            if (curStage == Stage.Anestrus && ticksToNextCycle > 0) return ticksToNextCycle / Configurations.CycleAcceleration;
            else return base.TicksToNextStage();
        }

        protected override void LutealAction()
        {
            base.LutealAction();
            if (curStage == Stage.Follicular)
                GoNextStage(Stage.Anestrus);
        }

        protected override void BleedingAction()
        {
            base.BleedingAction();
            if (curStage != Stage.Bleeding)
            {
                estrusflag = false;
                GoNextStage(Stage.Anestrus);
            }
        }

        protected override void PregnantAction()
        {
            base.PregnantAction();
            if (curStage != Stage.Pregnant)
                // Go halfway into the cycle
                ticksToNextCycle = (int)Utility.VariationRange(averageCycleIntervalTicks, cycleVariability) / 2;
        }

        protected override void RecoverAction()
        {
            base.RecoverAction();
            if (curStage != Stage.Recover)
                GoNextStage(Stage.Anestrus);
        }

        protected override void AnestrusAction()
        {
            if (ticksToNextCycle <= 0 && IsBreedingSeason())
            {
                ticksToNextCycle = (int)Utility.VariationRange(averageCycleIntervalTicks, cycleVariability);
                GoNextStage(Stage.Follicular);
            }
        }

        public override void CopyCycleProperties(HediffComp_Menstruation original)
        {
            base.CopyCycleProperties(original);
            if (original is HediffComp_PeriodicOvulator comp)
                averageCycleIntervalTicks = comp.averageCycleIntervalTicks;
        }
    }
}

license: unknown


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

namespace RJW_Menstruation
{
    public class CompProperties_Pheromones : HediffCompProperties
    {
        public float daysToMaxSeverity;
        public float range;

        public CompProperties_Pheromones()
        {
            compClass = typeof(HediffComp_Pheromones);
        }
    }

    public class HediffComp_Pheromones : HediffComp
    {
        public CompProperties_Pheromones Props => (CompProperties_Pheromones)props;
        public const int emitInterval = GenTicks.TickRareInterval;
        public const float emitRatio = (float)emitInterval / GenDate.TicksPerDay;

        public override void CompPostTick(ref float severityAdjustment)
        {
            try
            {
                if (!Configurations.EnablePheromones) return;
                if (!Pawn.RaceProps.Humanlike && (!Configurations.EnableAnimalCycle || Configurations.AnimalPheromoneEffect <= 0.0f)) return;
                if (!Pawn.IsHashIntervalTick(emitInterval)) return;
                if (!Pawn.SpawnedOrAnyParentSpawned) return;

                foreach (Pawn pawn in AffectedPawns())
                    ApplyEffectToPawn(pawn);
            }
            catch (Exception ex)
            {
                Log.Error($"Error when trying to emit pheromones from pawn {Pawn}: {ex}");
            }
        }

        protected IEnumerable<Pawn> AffectedPawns()
        {
            if (Pawn.GetCaravan() is Caravan caravan)
            {
                foreach (Pawn p in caravan.PawnsListForReading.Where(p => p != Pawn)) yield return p;
                yield break;
            }
            Map mapHeld = Pawn.MapHeld;
            if (mapHeld == null) yield break;
            foreach (Pawn pawn in mapHeld.mapPawns.AllPawnsSpawned)
            {
                if (pawn == Pawn) continue;
                if (!pawn.RaceProps.Humanlike) continue;
                if (Pawn.PositionHeld.InHorDistOf(pawn.PositionHeld, Props.range) && Pawn.GetRoom() == pawn.GetRoom())
                    yield return pawn;
            }
        }

        protected float GetEffectOnPawn(Pawn target)
        {
            if (target.Suspended || target.Dead) return 0.0f;
            if (target.needs?.TryGetNeed(VariousDefOf.SexNeed) == null) return 0.0f;
            if (!xxx.can_do_loving(target)) return 0.0f;
            if (target.story?.traits.HasTrait(TraitDefOf.Asexual) ?? true) return 0.0f;
            float penisEffect;
            if (Genital_Helper.has_penis_fertile(target)) penisEffect = 1.0f;
            else if (target.gender == Gender.Male) penisEffect = 0.25f;
            else return 0.0f;

            BodyDef body = target.RaceProps.body;
            if (!PawnCapacityUtility.BodyCanEverDoCapacity(body, PawnCapacityDefOf.Breathing)) return 0.0f;
            HediffSet hediffs = target.health.hediffSet;
            float breathingEffect = PawnCapacityUtility.CalculateCapacityLevel(hediffs, PawnCapacityDefOf.Breathing);
            float noseEffect = 1.0f;
            List<BodyPartRecord> noses = body.GetPartsWithDef(VariousDefOf.Nose);
            if (noses.Any())    // if the body doesn't have a nose but still breathes, then let it be affected
                noseEffect = noses.Average(nose => PawnCapacityUtility.CalculatePartEfficiency(hediffs, nose));

            return penisEffect * breathingEffect * noseEffect;
        }

        protected void ApplyEffectToPawn(Pawn target)
        {
            float intensity = GetEffectOnPawn(target);
            if (intensity <= 0.0f) return;
            Hediff pheromones = target.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_AffectedByPheromones);
            float decay = VariousDefOf.Hediff_AffectedByPheromones.CompProps<HediffCompProperties_SeverityPerDay>().severityPerDay;
            float raiseSeverityPerDay = intensity / Props.daysToMaxSeverity - decay; // Desired increase plus enough to overcome pheromone decay
            if (!Pawn.RaceProps.Humanlike) raiseSeverityPerDay *= Configurations.AnimalPheromoneEffect;
            float amountToApply = emitRatio * raiseSeverityPerDay;
            if (pheromones != null)
                pheromones.Severity += amountToApply;
            else
            {
                pheromones = HediffMaker.MakeHediff(VariousDefOf.Hediff_AffectedByPheromones, target);
                pheromones.Severity = amountToApply;
                target.health.AddHediff(pheromones);
            }
            return;
        }
    }
}