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

namespace RJW_Menstruation
{
    public class FertPillOutcomeDoer : IngestionOutcomeDoer
    {
        protected override void DoIngestionOutcomeSpecial(Pawn pawn, Thing ingested, int ingestedcount)
        {
            foreach (HediffComp_Menstruation comp in pawn.GetMenstruationComps())
                if (comp.curStage.Equals(HediffComp_Menstruation.Stage.Follicular)
                || comp.curStage.Equals(HediffComp_Menstruation.Stage.Luteal)
                || comp.curStage.Equals(HediffComp_Menstruation.Stage.Anestrus)
                )
                {
                    comp.SetEstrus();
                    comp.GoNextStage(HediffComp_Menstruation.Stage.Ovulatory);
                    comp.ovarypower--;
                }
        }
    }

    public class InduceOvulationOutcomeDoer : IngestionOutcomeDoer
    {
        protected override void DoIngestionOutcomeSpecial(Pawn pawn, Thing ingested, int ingestedcount)
        {
            foreach (HediffComp_Menstruation comp in pawn.GetMenstruationComps())
                if (comp.curStage.Equals(HediffComp_Menstruation.Stage.Follicular)
                    || comp.curStage.Equals(HediffComp_Menstruation.Stage.Anestrus)
                    )
                {
                    comp.SetEstrus();
                    comp.GoNextStage(HediffComp_Menstruation.Stage.Ovulatory);
                    comp.eggstack += ingestedcount - 1;
                }
        }
    }

    public class IngestionOutcomeDoer_AdjustSeverity : IngestionOutcomeDoer
    {
        public HediffDef hediffDef;
        public float severity;

        protected override void DoIngestionOutcomeSpecial(Pawn pawn, Thing ingested, int ingestedcount)
        {
            Hediff hediff = pawn.health.hediffSet.GetFirstHediffOfDef(hediffDef);
            if (hediff != null) hediff.Severity += severity * ingestedcount;
        }

    }

    public class OvaryPillOutcomeDoer : IngestionOutcomeDoer
    {
        public float effectOffset;

        protected override void DoIngestionOutcomeSpecial(Pawn pawn, Thing ingested, int ingestedcount)
        {
            foreach (HediffComp_Menstruation comp in pawn.GetMenstruationComps())
                if (Configurations.EnableMenopause) comp.RecoverOvary(1 + effectOffset);
        }
    }

    public class SuperOvulationOutcomeDoer : IngestionOutcomeDoer
    {
        protected override void DoIngestionOutcomeSpecial(Pawn pawn, Thing ingested, int ingestedcount)
        {
            foreach (HediffComp_Menstruation comp in pawn.GetMenstruationComps())
                comp.eggstack += Rand.Range(ingestedcount, 4 * ingestedcount);
        }
    }

    public class ContraceptiveOutcomeDoer : IngestionOutcomeDoer
    {
        protected override void DoIngestionOutcomeSpecial(Pawn pawn, Thing ingested, int ingestedcount)
        {

            List<Thought_Memory> memories = pawn.needs?.mood?.thoughts?.memories?.Memories.FindAll(
                x =>
                x.def == VariousDefOf.CameInsideF
             || x.def == VariousDefOf.CameInsideFFetish
             || x.def == VariousDefOf.HaterCameInsideF);
            if (memories.NullOrEmpty()) return;
            foreach (Thought_Memory m in memories)
            {
                if (m.def == VariousDefOf.HaterCameInsideF) m.moodPowerFactor = 0.5f;
                else m.moodPowerFactor = 0.3f;

            }
            if (pawn.IsProPregnancy(out _)) pawn.needs.mood.thoughts.memories.TryGainMemoryFast(VariousDefOf.HateTookContraceptivePill);
            else pawn.needs.mood.thoughts.memories.TryGainMemoryFast(VariousDefOf.TookContraceptivePill);
        }
    }
}


code:
using rjw;
using rjw.Modules.Interactions.Contexts;
using rjw.Modules.Interactions.Enums;
using rjw.Modules.Interactions.Rules.PartKindUsageRules;
using rjw.Modules.Shared;
using System.Collections.Generic;
using Verse;

namespace RJW_Menstruation.Interactions
{
    public class EstrusPartKindUsageRule : IPartPreferenceRule
    {
        private const float visible_estrus_multiplier = Multipliers.Doubled;
        private const float concealed_estrus_multiplier = 1.1f;

        private bool WillingAndAble(Pawn fucker, Pawn fucked, bool visible)
        {
            return
                fucked.IsInEstrus(visible)
                &&
                PregnancyHelper.CanImpregnate(fucker, fucked);
        }

        public IEnumerable<Weighted<LewdablePartKind>> ModifiersForDominant(InteractionContext context)
        {
            if (WillingAndAble(context.Internals.Submissive.Pawn, context.Internals.Dominant.Pawn, true))
                yield return new Weighted<LewdablePartKind>(visible_estrus_multiplier, LewdablePartKind.Vagina);
            else if (WillingAndAble(context.Internals.Submissive.Pawn, context.Internals.Dominant.Pawn, false))
                yield return new Weighted<LewdablePartKind>(concealed_estrus_multiplier, LewdablePartKind.Vagina);
        }

        public IEnumerable<Weighted<LewdablePartKind>> ModifiersForSubmissive(InteractionContext context)
        {
            if (WillingAndAble(context.Internals.Dominant.Pawn, context.Internals.Submissive.Pawn, true))
                yield return new Weighted<LewdablePartKind>(visible_estrus_multiplier, LewdablePartKind.Vagina);
            else if (WillingAndAble(context.Internals.Dominant.Pawn, context.Internals.Submissive.Pawn, false))
                yield return new Weighted<LewdablePartKind>(concealed_estrus_multiplier, LewdablePartKind.Vagina);
        }
    }
}

license: unknown


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

namespace RJW_Menstruation
{
    public class FilthMaker_Colored
    {

        public static bool TryMakeFilth(IntVec3 c, Map map, ThingDef filthDef, IEnumerable<string> sources, Color color, bool shouldPropagate, FilthSourceFlags additionalFlags = FilthSourceFlags.None)
        {
            Filth_Colored filth = (Filth_Colored)(from t in c.GetThingList(map)
                                                  where t.def == filthDef
                                                  select t).FirstOrDefault<Thing>();
            if (!c.Walkable(map) || (filth != null && !filth.CanBeThickened))
            {
                if (shouldPropagate)
                {
                    List<IntVec3> list = GenAdj.AdjacentCells8WayRandomized();
                    for (int i = 0; i < 8; i++)
                    {
                        IntVec3 c2 = c + list[i];
                        if (c2.InBounds(map) && TryMakeFilth(c2, map, filthDef, sources, color, false, FilthSourceFlags.None))
                        {
                            return true;
                        }
                    }
                }
                filth?.AddSources(sources);
                return false;
            }
            if (filth != null)
            {
                filth.ThickenFilth();
                filth.AddSources(sources);
            }
            else
            {
                if (!FilthMaker.CanMakeFilth(c, map, filthDef, additionalFlags))
                {
                    return false;
                }
                Filth_Colored filth2 = (Filth_Colored)ThingMaker.MakeThing(filthDef, null);
                filth2.DrawColor = color;
                filth2.AddSources(sources);
                GenSpawn.Spawn(filth2, c, map, WipeMode.Vanish);
            }
            //FilthMonitor.Notify_FilthSpawned();
            return true;
        }

    }
}


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

namespace RJW_Menstruation
{
    public class CompProperties_Breast : HediffCompProperties
    {
        public static readonly ColorInt DefaultBlacknippleColor = new ColorInt(55, 20, 0);
        public string BreastTex = "Breasts/Breast";
        public ColorInt BlacknippleColor = new ColorInt(55, 20, 0);

        public Color BlackNippleColor
        {
            get
            {
                return BlacknippleColor.ToColor;
            }
        }

        public CompProperties_Breast()
        {
            compClass = typeof(HediffComp_Breast);
        }
    }

    public class HediffComp_Breast : HediffComp
    {
        public const int tickInterval = GenDate.TicksPerHour * 3 / 2;
        public const float breastGrowthStart = 1f / 6f;
        public const float breastGrowthEnd = 1f / 3f;
        public static readonly SimpleCurve nippleTransitions = new SimpleCurve()
        {
            new CurvePoint(0f,0f),
            new CurvePoint(0.1f,0f),
            new CurvePoint(0.333f,0.167f),
            new CurvePoint(0.667f,0.833f),
            new CurvePoint(1.0f,1.0f)
        };
        public const float nippleChange = 0.2f;

        public CompProperties_Breast Props;

        HediffComp_SexPart sexPartComp;

        protected long ageOfLastBirth = 0;
        protected float maxBreastIncrement = -1f;
        protected float breastSizeIncreased = 0f;
        protected string debugGrowthStatus = "(Growth/shrink not yet calculated; run for 1.5h to update)";
        protected float nippleProgress = 0f;
        protected float baseAlpha = -1f;    // Will grow in response to pregnancy
        protected float baseAreola = -1f;
        protected float baseNipple = -1f;
        protected float cachedAlpha = -1f;  // Calculated dynamically instead of saved
        protected float cachedAreola = -1f; // Actual size = these * breast size
        protected float cachedNipple = -1f;
        protected float babyHalfAge = -1f;

        protected Color cachedColor;
        protected bool loaded = false;

        protected float BabyHalfAge
        {
            get
            {
                if (babyHalfAge > 0f) return babyHalfAge;
                List<LifeStageAge> ages = Pawn.RaceProps.lifeStageAges;
                if (ages?.Count > 1)
                    babyHalfAge = ages[1].minAge / 2;

                if (babyHalfAge <= 0) babyHalfAge = 1.2f / 2; // Default to human

                return babyHalfAge;
            }
        }

        protected void ShrinkBreasts()
        {
            // The natural rate will take them from full to empty during the second half of their child's babyhood
            float shrinkRate = tickInterval * MaxBreastIncrement / (BabyHalfAge * GenDate.TicksPerYear);
            float shrinkAmount = Mathf.Min(shrinkRate, breastSizeIncreased);
            breastSizeIncreased -= shrinkAmount;
            sexPartComp.baseSize -= shrinkAmount;
            if (sexPartComp.baseSize < 0.01f) sexPartComp.baseSize = 0.01f;
            sexPartComp.UpdateSeverity();
        }

        protected float MaxBreastIncrement
        {
            get
            {
                return maxBreastIncrement * Configurations.MaxBreastIncrementFactor;
            }
        }

        public Color NippleColor
        {
            get
            {
                return cachedColor;
            }
        }
        public float Alpha
        {
            get
            {
                return cachedAlpha;
            }
        }
        public float NippleSize
        {
            get
            {
                return cachedNipple * sexPartComp.baseSize;
            }
        }
        public float AreolaSize
        {
            get
            {
                return cachedAreola * sexPartComp.baseSize;
            }
        }

        public float BreastSizeIncreased
        {
            get
            {
                return breastSizeIncreased;
            }
        }

        public override void CompExposeData()
        {
            base.CompExposeData();
            Scribe_Values.Look(ref ageOfLastBirth, "ageOfLastBirth", -1);
            Scribe_Values.Look(ref maxBreastIncrement, "maxBreastIncrement", maxBreastIncrement, true);
            Scribe_Values.Look(ref breastSizeIncreased, "breastSizeIncreased", 0.0f);
            Scribe_Values.Look(ref nippleProgress, "nippleProgress", 0.0f);
            Scribe_Values.Look(ref baseAlpha, "baseAlpha", baseAlpha, true);
            Scribe_Values.Look(ref baseAreola, "baseAreola", baseAreola, true);
            Scribe_Values.Look(ref baseNipple, "baseNipple", baseNipple, true);
            if (Scribe.mode == LoadSaveMode.PostLoadInit)
                Initialize();
        }

        public bool ShouldSimulate()
        {
            if (!Pawn.ShouldCycle()) return false;
            if (Pawn.SpawnedOrAnyParentSpawned || 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 (
                    !Pawn.IsHashIntervalTick(tickInterval) ||
                    !ShouldSimulate()
                    )
                {
                    return;
                }
                CalculateBreastSize();
                CalculateNipples();
                UpdateNipples();
            }
            catch (Exception ex)
            {
                Log.Error($"Error processing breasts of {Pawn}: {ex}");
            }

        }

        public override void CompPostPostAdd(DamageInfo? dinfo)
        {
            if (!loaded) Initialize();
            if (ageOfLastBirth > Pawn.ageTracker.AgeChronologicalTicks) ageOfLastBirth = CalculateLastBirth();   // catch transplant issues
        }

        public override void CompPostPostRemoved()
        {
            if (Pawn.health.hediffSet.hediffs.Contains(parent))
            {
                Log.Warning($"Attempted to remove breast comp from wrong pawn ({Pawn}).");
                return;
            }
            base.CompPostPostRemoved();
        }

        protected long CalculateLastBirth()
        {
            long youngestAge = -1;

            if (Pawn.relations == null) return youngestAge;

            List<Hediff_BasePregnancy> rjwPregnancies = new List<Hediff_BasePregnancy>();
            Pawn.health.hediffSet.GetHediffs(ref rjwPregnancies);

            bool hasChild = Pawn.relations.Children.
                Where(child => !rjwPregnancies.Any(preg => preg.babies.Contains(child))). // no fetuses
                Where(child => child.GetMother() == Pawn).  // not Dad
                TryMinBy(child => child.ageTracker.AgeBiologicalTicks, out Pawn youngest);

            if (hasChild) youngestAge = Math.Max(Pawn.ageTracker.AgeBiologicalTicks - youngest.ageTracker.AgeBiologicalTicks, -1);

            return youngestAge;
        }

        public void Initialize()
        {
            Props = (CompProperties_Breast)props;
            sexPartComp = parent.TryGetComp<HediffComp_SexPart>();
            if (sexPartComp == null)
                Log.Error($"Could not get breast SexPart comp of {Pawn}");

            if (maxBreastIncrement <= 0f)
            {
                maxBreastIncrement = Utility.RandGaussianLike(0.088f, 0.202f);
            }
            if (ageOfLastBirth == 0 || ageOfLastBirth < -1)
            {
                ageOfLastBirth = CalculateLastBirth();
            }
            if (baseAlpha <= 0f)
            {
                baseAlpha = Utility.RandGaussianLike(0.0f, 0.3f) + Rand.Range(0.0f, 0.5f);
            }
            if (baseAreola <= 0f)
            {
                baseAreola = Utility.RandGaussianLike(0.0f, 1.0f);
            }
            if (baseNipple <= 0f)
            {
                baseNipple = Utility.RandGaussianLike(0.0f, 1.0f);
            }
            UpdateNipples();
            loaded = true;
        }

        protected void CalculateBreastSize()
        {
            // Ageless pawns can't depend on the chrono age, so just disable their growth entirely
            if (Pawn.ageTracker.BiologicalTicksPerTick <= 0f && breastSizeIncreased > 0)
            {
                ShrinkBreasts();
                debugGrowthStatus = "Base size (ageless)";
            }
            // The youngest child is less than halfway into babyhood: Full size
            else if (ageOfLastBirth > 0 && ageOfLastBirth + BabyHalfAge * GenDate.TicksPerYear > Pawn.ageTracker.AgeBiologicalTicks)
            {
                debugGrowthStatus = "Full size due to young child";
                if (breastSizeIncreased < MaxBreastIncrement)
                {
                    sexPartComp.baseSize += (MaxBreastIncrement - breastSizeIncreased);
                    sexPartComp.UpdateSeverity();
                    breastSizeIncreased = MaxBreastIncrement;
                }
            }
            // Pregnant, grow in the second half of first trimester
            else if (Pawn.IsRJWPregnant() || Pawn.IsBiotechPregnant())
            {
                float pregnancySize = Mathf.InverseLerp(breastGrowthStart, breastGrowthEnd, Pawn.GetFarthestPregnancyProgress()) * MaxBreastIncrement;
                if (breastSizeIncreased > pregnancySize)
                {
                    debugGrowthStatus = "Shrinking due to being oversize for pregnancy";
                    // Breasts still large from the last kid
                    ShrinkBreasts();
                }
                else if (breastSizeIncreased < MaxBreastIncrement)
                {
                    // Time to grow
                    float growAmount = pregnancySize - breastSizeIncreased;
                    if (growAmount != 0)
                        debugGrowthStatus = "Growing due to pregnancy";
                    else
                        debugGrowthStatus = "Pregnant, but not time to grow";
                    breastSizeIncreased += growAmount;
                    sexPartComp.baseSize += growAmount;
                    sexPartComp.UpdateSeverity();
                }
                else debugGrowthStatus = "Pregnant and full size";
            }
            // Not (or very early) pregnant and youngest child nonexistent or more than halfway into babyhood, time to shrink
            else if (breastSizeIncreased > 0)
            {
                debugGrowthStatus = "Shrinking due to no pregnancy nor young child";
                ShrinkBreasts();
            }
            else debugGrowthStatus = "Base size";
        }

        protected void CalculateNipples()
        {
            float newNippleProgress;
            if (Pawn.ageTracker.BiologicalTicksPerTick <= 0f)
                newNippleProgress = 0f;
            else if (ageOfLastBirth > 0 && ageOfLastBirth + BabyHalfAge * GenDate.TicksPerYear > Pawn.ageTracker.AgeBiologicalTicks)
                newNippleProgress = 1f;
            else if (Pawn.IsRJWPregnant() || Pawn.IsBiotechPregnant())
                newNippleProgress = nippleTransitions.Evaluate(Pawn.GetFarthestPregnancyProgress());
            else
                newNippleProgress = 0f;

            if (newNippleProgress < 0) newNippleProgress = 0;

            if (newNippleProgress == nippleProgress) return;    // Nothing to change
            else if (newNippleProgress > nippleProgress)
            {
                float progressDifference = newNippleProgress - nippleProgress;
                // All nipple growth has a slight effect on the base
                // Not mathematically precise in hitting the goal at the end of the term, but close enough
                baseAlpha *= 1.0f + progressDifference * Configurations.PermanentNippleChange;
                if (baseAlpha > 1.0f) baseAlpha = 1.0f;
                baseAreola *= 1.0f + progressDifference * Configurations.PermanentNippleChange;
                if (baseAreola > 1.0f) baseAreola = 1.0f;
                baseNipple *= 1.0f + progressDifference * Configurations.PermanentNippleChange;
                if (baseNipple > 1.0f) baseNipple = 1.0f;
                nippleProgress = newNippleProgress;
            }
            else
            {
                nippleProgress -= tickInterval / (BabyHalfAge * GenDate.TicksPerYear);
                if (nippleProgress < newNippleProgress) nippleProgress = newNippleProgress;
            }
        }

        public void GaveBirth()
        {
            ageOfLastBirth = Pawn.ageTracker.AgeBiologicalTicks;
        }

        public void AdjustNippleProgress(float amount)
        {
            nippleProgress = Mathf.Clamp01(nippleProgress + amount);
            UpdateNipples();
        }

        public void AdjustNippleSizeImmediately(float amount)
        {
            baseNipple = Mathf.Clamp01(baseNipple + amount);
            UpdateNipples();
        }

        public void AdjustNippleColorImmediately(float amount)
        {
            baseAlpha = Mathf.Clamp01(baseAlpha + amount);
            UpdateNipples();
        }

        public void AdjustAreolaSizeImmediately(float amount)
        {
            baseAreola = Mathf.Clamp01(baseAreola + amount);
            UpdateNipples();
        }

        public void UpdateNipples()
        {
            cachedAlpha = baseAlpha + nippleProgress * nippleChange;
            cachedAreola = baseAreola + nippleProgress * nippleChange;
            cachedNipple = baseNipple + nippleProgress * nippleChange;

            // For some reason, Props can go null when RJW relocates the chest (e.g. some animals), so catch that
            cachedColor = Colors.CMYKLerp(Utility.SafeSkinColor(Pawn), (Props?.BlackNippleColor ?? CompProperties_Breast.DefaultBlacknippleColor.ToColor), Alpha);
        }

        public void CopyBreastProperties(HediffComp_Breast original)
        {
            maxBreastIncrement = original.maxBreastIncrement;
            baseAlpha = original.baseAlpha;
            baseAreola = original.baseAreola;
            baseNipple = original.baseNipple;
            UpdateNipples();
        }

        public string DebugInfo()
        {
            return "Size: " + sexPartComp.baseSize +
                "\nIncrease: " + breastSizeIncreased +
                "\n" + debugGrowthStatus +
                "\nNipple progress: " + nippleProgress +
                "\nBase alpha: " + baseAlpha +
                "\nAlpha: " + cachedAlpha +
                "\nBase areola: " + baseAreola +
                "\nAreola: " + cachedAreola +
                "\nDisplayed areola: " + AreolaSize +
                "\nBase nipple: " + baseNipple +
                "\nNipple: " + cachedNipple +
                "\nDisplayed nipple: " + NippleSize;
        }
    }
}


code:
using RimWorld;
using UnityEngine;
using Verse;

namespace RJW_Menstruation
{
    public class CompProperties_InducedOvulator : CompProperties_Menstruation
    {
        public CompProperties_InducedOvulator()
        {
            compClass = typeof(HediffComp_InducedOvulator);
        }

    }

    public class HediffComp_InducedOvulator : HediffComp_Menstruation
    {
        protected bool hadOvulatoryStage = false;   // Regardless of whether an egg was actually produced

        protected override void PostInitialize()
        {
            base.PostInitialize();
            hadOvulatoryStage |= IsEggExist;
        }

        public override string GetCurStageDesc
        {
            get
            {
                switch (CurrentVisibleStage)
                {
                    case Stage.Follicular:
                        return Translations.Stage_Follicular_Induced_Desc + (EggHealth < 1f ? " " + Translations.Stage_Climacteric_Desc : "");
                    default:
                        return base.GetCurStageDesc;
                }
            }
        }

        // In an induced ovulator, about 65% of eggs ovulated will become pregnancies, so the expected lifetime supply
        // is the number of pregnancies they could have, with some extra to be sure.
        // An IUD or a poor fertility mate could run this down quicker. Oh well.
        protected override float RaceCyclesPerYear()
        {
            // Don't bother with breeding season, since so much time is planned to be spent pregnant.
            float pregnanciesPerYear = GenDate.DaysPerYear / Mathf.Max(1, Pawn.RaceProps.gestationPeriodDays);

            return 2 * pregnanciesPerYear / Configurations.ImplantationChanceDefault;
        }

        // There's really no good way to estimate the number of times it's been induced, so this is all we can do
        protected override int PawnEggsUsed(float pawnCyclesElapsed, float avglittersize)
        {
            return Mathf.CeilToInt((Pawn.relations?.ChildrenCount ?? 0) / Configurations.ImplantationChanceDefault);
        }

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

        protected override void GoOvulatoryStage()
        {
            estrusflag = false;
            hadOvulatoryStage = false;
            GoNextStage(Stage.Luteal);
        }

        protected override void OvulatoryAction()
        {
            base.OvulatoryAction();
            hadOvulatoryStage = true;
        }

        protected override void LutealAction()
        {
            base.LutealAction();
            if (curStage != Stage.Luteal) hadOvulatoryStage = false;
        }

        protected override void AfterCumIn(Pawn cummer)
        {
            base.AfterCumIn(cummer);
            if (curStage == Stage.Follicular) GoNextStage(Stage.Ovulatory);
        }

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

                switch (curStage)
                {
                    case Stage.Follicular:
                    case Stage.Ovulatory:
                        return true;
                    case Stage.Luteal:
                        return hadOvulatoryStage && curStageTicks < EggLifespanTicks;
                    default:
                        return false;
                }
            }
        }

        protected override bool ShouldBeInEstrus()
        {
            if (curStage == Stage.Luteal && !hadOvulatoryStage) return false;
            else return base.ShouldBeInEstrus();
        }

        protected override float RandomOvulationChance => 0;
    }
}