using RimWorld; using RimWorld.Planet; using rjw; using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; using Verse; namespace RJW_Menstruation { [Flags] public enum SeasonalBreed { Always = 0, Spring = 1, Summer = 2, Fall = 4, Winter = 8, FirstHalf = Spring | Summer, SecondHalf = Fall | Winter } public class CompProperties_Menstruation : HediffCompProperties { public float maxCumCapacity; // ml public float baseImplantationChanceFactor; public float basefertilizationChanceFactor; public int follicularIntervalDays = 14; //before ovulation including beginning of bleeding public int lutealIntervalDays = 14; //after ovulation until bleeding public int bleedingIntervalDays = 6; //must be less than folicularIntervalDays public int recoveryIntervalDays = 10; //additional infertile days after gave birth public int eggLifespanDays = 2; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday public string wombTex = "Womb/Womb"; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday public string vagTex = "Genitals/Vagina"; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday public bool infertile = false; public bool concealedEstrus = false; public SeasonalBreed breedingSeason = SeasonalBreed.Always; public int estrusDaysBeforeOvulation = 3; public int eggMultiplier = 1; public CompProperties_Menstruation() { compClass = typeof(HediffComp_Menstruation); } } public class CompProperties_Anus : HediffCompProperties { public string analTex = "Genitals/Anal"; public CompProperties_Anus() { compClass = typeof(HediffComp_Anus); } } public class HediffComp_Menstruation : HediffComp { const float minmakefilthvalue = 1.0f; //const int ovarypowerthreshold = 72; const int tickInterval = GenDate.TicksPerHour; const int maxImplantDelayHours = 30 * 24; const int minImplantAgeHours = 3 * 24; const float pulloutSuccessRate = 0.8f; const float fetishPulloutSuccessModifier = 0.25f; public CompProperties_Menstruation Props; public Stage curStage = Stage.Follicular; public int curStageHrs = 0; public bool loaded = false; public bool initError = false; public int ovarypower = -100000; public int eggstack = 0; public bool DoCleanWomb = false; public enum Stage { Follicular, Ovulatory, Luteal, Bleeding, Pregnant, Recover, None, Infertile, Anestrus } public enum EstrusLevel { None, Concealed, Visible } public static readonly Dictionary StageTexture = new Dictionary() { { Stage.Follicular, TextureCache.FollicularTexture }, { Stage.Luteal, TextureCache.LutealTexture }, { Stage.Bleeding, TextureCache.BleedingTexture }, { Stage.Pregnant, TextureCache.PregnantTexture }, { Stage.Recover, TextureCache.RecoverTexture } }; protected List cums; protected List eggs; protected float cycleSpeed = -1; protected float cycleVariability = -1; protected int currentIntervalHours = -1; protected float crampPain = -1; protected Need sexNeed = null; protected string customwombtex = null; protected string customvagtex = null; protected bool estrusflag = false; protected int opcache = -1; protected float antisperm = 0.0f; protected float? originvagsize = null; // RJW pregnancy, or Biotech pregnancy/labor/laborpushing protected Hediff pregnancy = null; protected int eggLifeSpanHours = 48; 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) }; 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 > 0) return opcache; float avglittersize; try { avglittersize = Mathf.Max(Rand.ByCurveAverage(Pawn.def.race.litterSizeCurve), 1.0f); } catch { // Any exceptions in that will have been reported elsewhere in the code by now avglittersize = 1.0f; }; avglittersize *= ovulationFactor; const float yearsBeforeMenopause = 6.0f; opcache = (int)(RaceCyclesPerYear() * avglittersize * yearsBeforeMenopause * (Pawn.def.race.lifeExpectancy / ThingDefOf.Human.race.lifeExpectancy)); if (opcache == 0) opcache = 1; return opcache; } } // >= 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 { return cums?.Sum(cum => cum.Volume) ?? 0; } } public float TotalFertCum { get { return cums?.Sum(cum => cum.FertVolume) ?? 0; } } public float TotalCumPercent { get { return cums?.Sum(cum => cum.Volume) / Props.maxCumCapacity ?? 0; } } public float CumCapacity { get { float res = Props.maxCumCapacity * Pawn.BodySize; if (curStage != Stage.Pregnant || (pregnancy?.Severity ?? 0f) < 0.175f) res *= 500f; return res; } } //make follicular interval into half and double egg lifespan public float CycleFactor { get { if (xxx.has_quirk(Pawn, "Breeder")) return 0.5f; return 1.0f; } } // I hate doing this, but it's the least bad option public bool calculatingOvulationChance = false; public float OvulationChance { get { 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; // Replicate how rjw.PawnCapacityWorker_Fertility.CalculateCapacityLevel does it, but without the age factor if (!Pawn.RaceHasFertility()) return 0.0f; if (AndroidsCompatibility.IsAndroid(Pawn) && parent.def != Genital_Helper.archotech_vagina) return 0.0f; foreach (var part in StatDefOf.Fertility.parts) { if(part is StatPart_FertilityByGenderAge fertilityByAge) { float factor = 1.0f; fertilityByAge.TransformValue(StatRequest.For(Pawn), ref factor); if (factor <= 0.0f) return 0.0f; // Too young or too old } else part.TransformValue(StatRequest.For(Pawn), ref ovulationChance); } if (Pawn.HasQuirk(QuirkUtility.Quirks.Breeder)) ovulationChance *= 10.0f; try { calculatingOvulationChance = true; ovulationChance *= PawnCapacityUtility.CalculateCapacityLevel(Pawn.health.hediffSet, xxx.reproduction); } finally { calculatingOvulationChance = false; } } return ovulationChance; } } public float ImplantChance { get { float factor = 1.0f; if (ModsConfig.BiotechActive && xxx.is_human(Pawn)) { // Implant factor will be based solely on pawn age, plus any rollover from ovulation chance StatPart_FertilityByGenderAge fertilityStatPart = StatDefOf.Fertility.GetStatPart(); fertilityStatPart?.TransformValue(StatRequest.For(Pawn), ref factor); float ovulationOverflow = OvulationChance; if (ovulationOverflow > 1.0f) factor *= ovulationOverflow; return Props.baseImplantationChanceFactor * FertilityModifier * factor; } else { return Pawn.health.capacities.GetLevel(xxx.reproduction) * Props.baseImplantationChanceFactor * FertilityModifier * factor; } } } public IEnumerable GetCumsInfo { get { if (cums.NullOrEmpty()) yield return Translations.Info_noCum; else foreach (Cum cum in cums) { if (!cum.notcum) yield return string.Format("{0}: {1:0.##}ml", cum.pawn?.Label, cum.Volume); else yield return string.Format("{0}: {1:0.##}ml", cum.notcumLabel, cum.Volume); } } } public Color GetCumMixtureColor { get { Color mixedcolor = Color.white; if (cums.NullOrEmpty()) return mixedcolor; float mixedsofar = 0; foreach (Cum cum in cums) { if (cum.Volume > 0) { mixedcolor = Colors.CMYKLerp(mixedcolor, cum.Color, cum.Volume / (mixedsofar + cum.Volume)); mixedsofar += cum.Volume; } } return mixedcolor; } } public Stage CurrentVisibleStage { get { if (curStage == Stage.Pregnant) { if (Configurations.InfoDetail == Configurations.DetailLevel.All || (pregnancy?.Visible ?? false)) return Stage.Pregnant; else return Stage.Luteal; } return curStage; } } public string GetCurStageLabel { get { switch (CurrentVisibleStage) { case Stage.Follicular: return Translations.Stage_Follicular + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : ""); case Stage.Ovulatory: return Translations.Stage_Ovulatory + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : ""); case Stage.Luteal: return Translations.Stage_Luteal + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : ""); case Stage.Bleeding: return Translations.Stage_Bleeding + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : ""); case Stage.Pregnant: return Translations.Stage_Pregnant; case Stage.Recover: return Translations.Stage_Recover; case Stage.None: case Stage.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 { return customwombtex ?? Props.wombTex; } set { customwombtex = value; } } public string VagTex { get { return customvagtex ?? Props.vagTex; } set { customvagtex = value; } } public string GetFertilizingInfo { get { if (eggs.NullOrEmpty()) return ""; StringBuilder res = new StringBuilder(); int fertilized = eggs.Count(egg => egg.fertilized); if (fertilized != 0) res.AppendFormat("{0} {1}", fertilized, Translations.Dialog_WombInfo05); if (fertilized != 0 && eggs.Count - fertilized != 0) res.Append(", "); if (cums.NullOrEmpty() || TotalFertCum == 0) { if (eggs.Count - fertilized != 0) res.AppendFormat("{0} {1}", eggs.Count - fertilized, Translations.Dialog_WombInfo07); } else { if (eggs.Count - fertilized != 0) res.AppendFormat("{0} {1}", eggs.Count - fertilized, Translations.Dialog_WombInfo06); } return res.ToString(); } } public bool IsEggFertilizing { get { if (eggs.NullOrEmpty()) return false; return cums?.Any(cum => cum.FertVolume > 0) ?? false; } } /// /// returns fertstage. if not fertilized returns -1 /// public int IsFertilized { get { if (eggs?.All(egg => !egg.fertilized) ?? true) return -1; return eggs.Max(egg => egg.fertstage); } } public IEnumerable GetCummersAndFertilizers() { if (!cums.NullOrEmpty()) foreach (Cum cum in cums) yield return cum.pawn; if (!eggs.NullOrEmpty()) foreach (Egg egg in eggs) yield return egg.fertilizer; } public bool IsEggExist { get { return !eggs.NullOrEmpty(); } } public int EggLifespanHours { get { return eggLifeSpanHours; } } public virtual bool IsDangerDay { get { if (Pawn.HasIUD()) return false; switch (curStage) { case Stage.Follicular: return curStageHrs > 0.7f * currentIntervalHours; case Stage.Ovulatory: return true; case Stage.Luteal: return curStageHrs < EggLifespanHours; default: return false; } } } public int GetNumOfEggs { get { return eggs?.Count ?? 0; } } public Color BloodColor { get { try { Color c = Pawn.def.race.BloodDef.graphicData.color; return c; } catch { return Colors.blood; } } } public float OriginVagSize { get { if (originvagsize == null) { originvagsize = parent.Severity; } return originvagsize ?? 0.1f; } set { originvagsize = value; } } public float CurStageIntervalHours { get { return currentIntervalHours; } } public float StageProgress { get { if (pregnancy == null) return Mathf.Clamp01(curStageHrs / CurStageIntervalHours); 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 * 24)); return GenMath.LerpDouble(0, discoveryTime, lutealProgressWhenImplanted, 1.0f, pregnancy.Severity); } } public Texture2D GetStageTexture { get { if (!StageTexture.TryGetValue(CurrentVisibleStage, out Texture2D tex)) tex = TextureCache.PregnantTexture; return tex; } } public override void CompExposeData() { base.CompExposeData(); Scribe_Collections.Look(ref cums, saveDestroyedThings: true, label: "cums", lookMode: LookMode.Deep, ctorArgs: new object[0]); Scribe_Collections.Look(ref eggs, saveDestroyedThings: true, label: "eggs", lookMode: LookMode.Deep, ctorArgs: new object[0]); Scribe_Values.Look(ref curStage, "curStage", curStage, true); Scribe_Values.Look(ref curStageHrs, "curStageHrs", curStageHrs, true); Scribe_Values.Look(ref cycleSpeed, "cycleSpeed", cycleSpeed, true); Scribe_Values.Look(ref cycleVariability, "cycleVariability", cycleVariability, true); Scribe_Values.Look(ref currentIntervalHours, "currentIntervalHours", currentIntervalHours, true); Scribe_Values.Look(ref crampPain, "crampPain", crampPain, true); Scribe_Values.Look(ref ovarypower, "ovarypower", ovarypower, true); Scribe_Values.Look(ref eggstack, "eggstack", eggstack, true); Scribe_Values.Look(ref estrusflag, "estrusflag", estrusflag, true); Scribe_Values.Look(ref originvagsize, "originvagsize", originvagsize, true); Scribe_Values.Look(ref DoCleanWomb, "DoCleanWomb", DoCleanWomb, true); Scribe_References.Look(ref pregnancy, "pregnancy"); } public override void CompPostPostAdd(DamageInfo? dinfo) { if (!loaded) { Initialize(); } } public void Notify_UpdatedGenes() { eggLifeSpanHours = Props.eggLifespanDays * 24; estrusLevel = Props.concealedEstrus ? EstrusLevel.Concealed : EstrusLevel.Visible; ovulationFactor = 1f; noBleeding = false; if (Pawn.genes == null || !ModsConfig.BiotechActive) return; if (Pawn.genes.HasGene(VariousDefOf.ShortEggLifetime)) eggLifeSpanHours = eggLifeSpanHours * 3 / 4; else if (Pawn.genes.HasGene(VariousDefOf.DoubleEggLifetime)) eggLifeSpanHours *= 2; else if (Pawn.genes.HasGene(VariousDefOf.QuadEggLifetime)) eggLifeSpanHours *= 4; if (Pawn.genes.HasGene(VariousDefOf.NeverEstrus)) estrusLevel = EstrusLevel.None; else if (Pawn.genes.HasGene(VariousDefOf.FullEstrus)) estrusLevel = EstrusLevel.Visible; if (Pawn.genes.HasGene(VariousDefOf.DoubleOvulation)) ovulationFactor = 2f; else if (Pawn.genes.HasGene(VariousDefOf.QuadOvulation)) ovulationFactor = 4f; noBleeding = Pawn.genes.HasGene(VariousDefOf.NoBleeding); } public bool ShouldSimulate() { if (!Configurations.EnableAnimalCycle && Pawn.IsAnimal()) return false; if (Pawn.Spawned || 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 (!ShouldSimulate()) return; // Initialize immediately if needed, but if there's an error, then don't spam it every tick if (!loaded && !initError) { Log.Warning($"{Pawn}'s womb is ticking, but was not initialized first"); Initialize(); } if (!Pawn.IsHashIntervalTick(tickInterval)) return; if (initError) Log.Warning($"Attempting to process {Pawn}'s womb uninitialized"); if (Pregnancy != null && curStage != Stage.Pregnant) { Log.Warning($"{Pawn}'s womb has a pregnancy, but was not in the pregnant stage"); GoNextStage(Stage.Pregnant); } BeforeSimulator(); if (ShouldBeInfertile()) GoNextStage(Stage.Infertile); 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 { 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 HoursToNextStage() { return Math.Max(0,(currentIntervalHours - curStageHrs) / Configurations.CycleAcceleration); } public override string CompDebugString() { if (curStage == Stage.None || curStage == Stage.Infertile || curStage == Stage.Pregnant) return base.CompDebugString(); StringBuilder debugString = new StringBuilder(); debugString.Append($"Time to next state: "); debugString.Append(GenDate.ToStringTicksToPeriod(HoursToNextStage() * GenDate.TicksPerHour)); return debugString.ToString(); } /// /// Get fluid in womb that not a cum /// /// /// public Cum GetNotCum(string notcumlabel) { if (!cums.NullOrEmpty()) foreach (Cum cum in cums) { if (cum.notcum && cum.notcumLabel.Equals(notcumlabel)) return cum; } return null; } /// /// Get pawn's cum in womb /// /// /// public Cum GetCum(Pawn pawn) { return cums?.Find(cum => !cum.notcum && cum.pawn == pawn); } /// /// Inject pawn's cum into womb /// /// /// /// /// public void CumIn(Pawn pawn, float volume, float fertility = 1.0f, bool precum = false) { if (volume <= 0) return; if (!precum && fertility > 0 && IsDangerDay && pawn.relations.GetPregnancyApproachForPartner(Pawn) == PregnancyApproach.AvoidPregnancy) { float successChance = pulloutSuccessRate; if (pawn.HasQuirk(QuirkUtility.Quirks.ImpregnationFetish)) successChance *= fetishPulloutSuccessModifier; if (Pawn.HasQuirk(QuirkUtility.Quirks.ImpregnationFetish)) 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(pawn)) { cum.MergeWithCum(volume, fertility); merged = true; } cum.DismishForce(cumoutrate); } if (!merged) cums.Add(new Cum(pawn, volume * (1 - cumoutrate), fertility)); } else { bool merged = false; if (!cums.NullOrEmpty()) foreach (Cum cum in cums) { if (cum.pawn.Equals(pawn)) { cum.MergeWithCum(volume, fertility); merged = true; } } if (!merged) cums.Add(new Cum(pawn, volume, fertility)); } cumd = TotalCumPercent - cumd; if (!precum) { Pawn.records.AddTo(VariousDefOf.AmountofCreampied, volume); AfterCumIn(pawn); AfterFluidIn(cumd); } } /// /// Inject pawn's fluid into womb /// /// /// /// /// /// public void CumIn(Pawn pawn, float volume, string notcumlabel, float decayresist = 0, ThingDef filthdef = null) { if (volume <= 0) return; float tmp = TotalCum + volume; float cumd = TotalCumPercent; if (tmp > CumCapacity) { float cumoutrate = 1 - (CumCapacity / tmp); bool merged = false; if (!cums.NullOrEmpty()) foreach (Cum cum in cums) { if (cum.notcum && cum.pawn.Equals(pawn) && cum.notcumLabel.Equals(notcumlabel)) { cum.MergeWithFluid(volume, decayresist, filthdef); merged = true; } cum.DismishForce(cumoutrate); } if (!merged) cums.Add(new Cum(pawn, volume * (1 - cumoutrate), notcumlabel, decayresist, filthdef)); } else { bool merged = false; if (!cums.NullOrEmpty()) foreach (Cum cum in cums) { if (cum.notcum && cum.pawn.Equals(pawn) && cum.notcumLabel.Equals(notcumlabel)) { cum.MergeWithFluid(volume, decayresist, filthdef); merged = true; } } if (!merged) cums.Add(new Cum(pawn, volume, notcumlabel, decayresist, filthdef)); } cumd = TotalCumPercent - cumd; AfterNotCumIn(); AfterFluidIn(cumd); } protected virtual void AfterCumIn(Pawn cummer) { ThoughtCumInside(cummer); TaleCumInside(cummer); } protected virtual void AfterNotCumIn() { } /// /// Action for both Cum and NotCum /// /// Fluid deviation 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 = (Absorber)Pawn.apparel?.WornApparel?.Find(x => x is Absorber); if (absorber != null) { absorber.WearEffect(); if (absorber.dirty && absorber.EffectAfterDirty) absorber.DirtyEffect(); } } /// /// For natural leaking /// protected virtual void AfterCumOut() { Pawn.needs?.mood?.thoughts?.memories?.TryGainMemory(VariousDefOf.LeakingFluids); } /// /// For all type of leaking /// /// protected virtual void AfterFluidOut(float fd) { } /// /// Excrete cums in womb naturally /// public void CumOut() { float leakfactor = 1.0f; float totalleak = 0f; float cumd = TotalCumPercent; List filthlabels = new List(); 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; HashSet removecums = new HashSet(); foreach (Cum cum in cums) { cum.CumEffects(Pawn); float vd = cum.DismishNatural(leakfactor, this, antisperm); cum.MakeThinner(Configurations.CycleAcceleration); totalleak += AbsorbCum(vd, absorber); string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString()); filthlabels.Add(tmp.Replace(" x1", "")); if (cum.ShouldRemove()) removecums.Add(cum); } if (cums.Count > 1) MakeCumFilthMixture(totalleak, filthlabels); else if (cums.Count == 1) MakeCumFilth(cums.First(), totalleak); cums.RemoveAll(cum => removecums.Contains(cum)); cumd = TotalCumPercent - cumd; if (totalleak >= 1.0f) AfterCumOut(); AfterFluidOut(cumd); } /// /// Force excrete cums in womb and get excreted amount of specific cum. /// /// /// /// Amount of target cum public float CumOut(Cum targetcum, float portion = 0.1f) { if (cums.NullOrEmpty()) return 0; float totalleak = 0; List filthlabels = new List(); float outcum = 0; float cumd = TotalCumPercent; HashSet removecums = new HashSet(); 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)); cumd = TotalCumPercent - cumd; AfterFluidOut(cumd); return outcum; } /// /// Force excrete cums in womb and get mixture of cum. /// /// /// /// public CumMixture MixtureOut(ThingDef mixtureDef, float portion = 0.1f) { if (cums.NullOrEmpty()) return null; Color color = GetCumMixtureColor; float totalleak = 0; List cumlabels = new List(); HashSet removecums = new HashSet(); 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)); return new CumMixture(Pawn, totalleak, cumlabels, color, mixtureDef, pure); } /// /// Debug: Remove all cums from a womb /// /// public void RemoveAllCums() { cums.Clear(); } /// /// Fertilize eggs and return the result /// /// 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; Notify_UpdatedGenes(); if (Props.infertile) { if (cums == null) cums = new List(); 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 (currentIntervalHours < 0) { if (ShouldBeInfertile()) curStage = Stage.Infertile; else if (!IsBreedingSeason()) curStage = Stage.Anestrus; else curStage = RandomStage(); if (curStage == Stage.Follicular) currentIntervalHours = PeriodRandomizer(Stage.Follicular) - PeriodRandomizer(Stage.Bleeding); else currentIntervalHours = PeriodRandomizer(curStage); if (currentIntervalHours <= 0) currentIntervalHours = 1; else if (currentIntervalHours < curStageHrs) curStageHrs = currentIntervalHours; } if (crampPain < 0) crampPain = PainRandomizer(); InitializeExtraValues(); if (cums == null) cums = new List(); if (eggs == null) eggs = new List(); TakeLoosePregnancy(); //Log.Message(Pawn.Label + " - Initialized menstruation comp"); loaded = true; initError = false; } protected virtual void InitializeExtraValues() { } protected virtual float RaceCyclesPerYear() { int breedingSeasons = 0; if (Props.breedingSeason == SeasonalBreed.Always) breedingSeasons = 4; else { if ((Props.breedingSeason & SeasonalBreed.Spring) != 0) breedingSeasons++; if ((Props.breedingSeason & SeasonalBreed.Summer) != 0) breedingSeasons++; if ((Props.breedingSeason & SeasonalBreed.Fall) != 0) breedingSeasons++; if ((Props.breedingSeason & SeasonalBreed.Winter) != 0) breedingSeasons++; } float breedingRatio = breedingSeasons / 4.0f; return breedingRatio * GenDate.DaysPerYear / ((float)(Props.follicularIntervalDays + Props.lutealIntervalDays) / Configurations.CycleAccelerationDefault); } protected virtual int PawnEggsUsed(float pawnCyclesElapsed, float avglittersize) { return (int)(pawnCyclesElapsed * avglittersize); } public int GetOvaryPowerByAge() { float avglittersize; try { avglittersize = Mathf.Max(Rand.ByCurveAverage(Pawn.def.race.litterSizeCurve), 1.0f); } catch (NullReferenceException) { avglittersize = 1.0f; } avglittersize *= ovulationFactor; float fertStartAge = Pawn.RaceProps.lifeStageAges?.Find(stage => stage.def.reproductive)?.minAge ?? 0.0f; float fertEndAge = Pawn.RaceProps.lifeExpectancy * (Pawn.IsAnimal() ? RJWPregnancySettings.fertility_endage_female_animal : RJWPregnancySettings.fertility_endage_female_humanlike); if (fertEndAge < fertStartAge) fertEndAge = fertStartAge; float raceCyclesPerYear = RaceCyclesPerYear(); int lifetimeCycles = (int)(raceCyclesPerYear * (fertEndAge - fertStartAge)); int lifetimeEggs = (int)(lifetimeCycles * avglittersize * Props.eggMultiplier * Utility.RandGaussianLike(0.70f, 1.30f, 5)); float pawnCyclesPerYear = raceCyclesPerYear * cycleSpeed; float pawnCyclesElapsed = Mathf.Max((Pawn.ageTracker.AgeBiologicalYearsFloat - fertStartAge) * pawnCyclesPerYear, 0.0f); int pawnEggsUsed = PawnEggsUsed(pawnCyclesElapsed, avglittersize); return Math.Max(lifetimeEggs - pawnEggsUsed, 0); } protected void InitOvary() { if (ovarypower < -50000) { ovarypower = GetOvaryPowerByAge(); if (Props.infertile) curStage = Stage.None; else if (ovarypower < 1) { curStage = Stage.Infertile; } } } public void RecoverOvary(float multiply = 1.2f) { ovarypower = Math.Max(0, (int)(ovarypower * multiply)); } protected virtual void BeforeSimulator() { CumOut(); } protected virtual void AfterSimulator() { if (EggHealth < 1f) { if (sexNeed == null) sexNeed = Pawn.needs.TryGetNeed(VariousDefOf.SexNeed); if (sexNeed?.CurLevel < 0.5) sexNeed.CurLevel += 0.01f / Math.Max(1, Pawn.GetMenstruationComps().Count()); } } protected virtual bool ShouldBeInEstrus() { if (!loaded) Initialize(); switch (curStage) { case Stage.Follicular: return curStageHrs > currentIntervalHours - Props.estrusDaysBeforeOvulation * 24; case Stage.Ovulatory: return true; case Stage.Luteal: return curStageHrs < EggLifespanHours; 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; } } protected Pawn Fertilize() { if (cums.NullOrEmpty()) return null; List eligibleCum = cums.FindAll(cum => !cum.notcum && cum.FertVolume > 0 && cum.pawn != null && (RJWPregnancySettings.bestial_pregnancy_enabled || xxx.is_animal(Pawn) == xxx.is_animal(cum.pawn))); if (eligibleCum.Count == 0) return null; float totalFertPower = eligibleCum.Sum(cum => cum.FertVolume); if (Rand.Chance(Mathf.Pow(1.0f - Configurations.FertilizeChance, totalFertPower * Props.basefertilizationChanceFactor))) return null; Pawn.records.AddTo(VariousDefOf.AmountofFertilizedEggs, 1); float selection = Rand.Range(0.0f, totalFertPower); foreach (Cum cum in eligibleCum) { selection -= cum.FertVolume; if (selection <= 0) return cum.pawn; } // We shouldn't reach here, but floating point errors exist, so just to be sure, select whomever came the most return eligibleCum.MaxBy(cum => cum.FertVolume).pawn; } protected bool Implant() { if (eggs.NullOrEmpty()) return false; HashSet deadeggs = new HashSet(); bool pregnant = false; foreach (Egg egg in eggs) { if (!egg.fertilized || egg.fertstage < minImplantAgeHours || egg.position < Math.Min(Props.lutealIntervalDays * 24 / 2, maxImplantDelayHours)) continue; else if (egg.fertilizer == null) { if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to null father"); deadeggs.Add(egg); continue; } else if (Pawn.health.hediffSet.GetFirstHediff() != 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 (Rand.Chance(Configurations.ImplantationChance * ImplantChance * InterspeciesImplantFactor(egg.fertilizer))) { if (Configurations.Debug) Log.Message($"Implanting fertilized egg of {Pawn} into {parent}, father {egg.fertilizer}"); if (pregnancy != null) { if (Configurations.PregnancySource == Configurations.PregnancyType.Biotech && Configurations.EnableBiotechTwins && Configurations.EnableHeteroOvularTwins) { if (Configurations.Debug) Log.Message($"Adding to existing Biotech pregnancy {pregnancy}"); HediffComp_PregeneratedBabies comp = pregnancy.TryGetComp(); if (comp == null) Log.Warning($"Trying to add Biotech egg to {Pawn}'s pregnancy without a pregenerated baby comp: {pregnancy}"); else { comp.AddNewBaby(Pawn, egg.fertilizer); pregnant = true; deadeggs.Add(egg); } } else if (Configurations.PregnancySource == Configurations.PregnancyType.MultiplePregnancy && Configurations.EnableHeteroOvularTwins) { if (pregnancy is Hediff_MultiplePregnancy h) { if (Configurations.Debug) Log.Message($"Adding to existing pregnancy {h}"); h.AddNewBaby(Pawn, egg.fertilizer); } pregnant = true; deadeggs.Add(egg); } else { pregnant = true; break; } } else { 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 pregnancies = new List(); 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(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().AddNewBaby(Pawn, egg.fertilizer); ((Hediff_Pregnant)pregnancy).SetParents(Pawn, egg.fertilizer, PregnancyUtility.GetInheritedGeneSet(egg.fertilizer, 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.fertstage / Configurations.CycleAcceleration * GenDate.TicksPerHour; rjw_preg.p_end_tick -= egg.fertstage / Configurations.CycleAcceleration * GenDate.TicksPerHour; } } } else { if (Configurations.Debug) Log.Message($"Fertilized egg of {Pawn} failed to implant (father {egg.fertilizer})"); deadeggs.Add(egg); } } if (pregnant && (Configurations.PregnancySource != Configurations.PregnancyType.MultiplePregnancy || !Configurations.EnableHeteroOvularTwins) && (Configurations.PregnancySource != Configurations.PregnancyType.Biotech || !Configurations.EnableBiotechTwins || !Configurations.EnableHeteroOvularTwins)) { eggs.Clear(); return true; } else eggs.RemoveAll(egg => deadeggs.Contains(egg)); return pregnant; } protected void BleedOut() { CumIn(Pawn, Rand.Range(0.02f * Configurations.BleedingAmount, 0.04f * Configurations.BleedingAmount), Translations.Menstrual_Blood, -5.0f, Pawn.def.race?.BloodDef ?? ThingDefOf.Filth_Blood); Cum blood = GetNotCum(Translations.Menstrual_Blood); if (blood != null) blood.Color = BloodColor; } /// /// Make filth ignoring absorber /// /// /// protected void MakeCumFilth(Cum cum, float amount) { if (Pawn.Map == null) return; if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn?.LabelShort ?? "Unknown"); } /// /// Absorb cum and return leaked amount /// /// /// /// /// protected float AbsorbCum(float amount, Absorber absorber) { if (absorber == null) { //if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort); return amount; } float absorbable = absorber.GetStatValue(VariousDefOf.MaxAbsorbable); absorber.SetColor(Colors.CMYKLerp(GetCumMixtureColor, absorber.DrawColor, 1f - amount / absorbable)); if (absorber.dirty) { //if (absorber.LeakAfterDirty) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort); return amount; } absorber.absorbedfluids += amount; if (absorber.absorbedfluids > absorbable && !Pawn.apparel.IsLocked(absorber)) { absorber.def = absorber.DirtyDef; //absorber.fluidColor = GetCumMixtureColor; absorber.dirty = true; } return 0; } protected float MakeCumFilthMixture(float amount, List cumlabels) { if (Pawn.Map == null) return 0; if (amount >= minmakefilthvalue) { FilthMaker_Colored.TryMakeFilth(Pawn.Position, Pawn.Map, VariousDefOf.FilthMixture, cumlabels, GetCumMixtureColor, false); } return amount; } protected void EggDecay() { HashSet deadeggs = new HashSet(); foreach (Egg egg in eggs) { egg.position += Configurations.CycleAcceleration; if (egg.fertilized) egg.fertstage += Configurations.CycleAcceleration; else { egg.lifespanhrs -= Configurations.CycleAcceleration; if (egg.lifespanhrs < 0) deadeggs.Add(egg); } } eggs.RemoveAll(egg => deadeggs.Contains(egg)); } protected void AddCrampPain() { Hediff hediff = HediffMaker.MakeHediff(VariousDefOf.Hediff_MenstrualCramp, Pawn); hediff.Severity = crampPain * Rand.Range(0.9f, 1.1f); HediffCompProperties_SeverityPerDay Prop = (HediffCompProperties_SeverityPerDay)hediff.TryGetComp().props; Prop.severityPerDay = -hediff.Severity / (currentIntervalHours / 24) * Configurations.CycleAcceleration; Pawn.health.AddHediff(hediff, Genital_Helper.get_genitalsBPR(Pawn)); } protected virtual void FollicularAction() { if (!IsBreedingSeason()) { estrusflag = false; GoNextStage(Stage.Anestrus); return; } else if (curStageHrs >= currentIntervalHours) { GoOvulatoryStage(); } else { curStageHrs += Configurations.CycleAcceleration; if (!estrusflag && curStageHrs > currentIntervalHours - Props.estrusDaysBeforeOvulation * 24) { estrusflag = true; SetEstrus(); } StayCurrentStage(); } } protected virtual void OvulatoryAction() { estrusflag = false; float eggnum; try { eggnum = Math.Max(Rand.ByCurve(Pawn.def.race.litterSizeCurve), 1f); } catch (NullReferenceException) { eggnum = 1f; } catch (ArgumentException e) { Log.Warning($"Invalid litterSizeCurve for {Pawn.def}: {e}"); eggnum = 1f; } eggnum *= ovulationFactor; int toOvulate = (int)eggnum + eggstack; float ovulationChance = OvulationChance; 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)(EggLifespanHours / 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 (curStageHrs >= currentIntervalHours) { 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 { curStageHrs += Configurations.CycleAcceleration; StayCurrentStage(); } } else { curStageHrs += Configurations.CycleAcceleration; StayCurrentStage(); } } protected virtual void BleedingAction() { if (curStageHrs >= currentIntervalHours) { Hediff hediff = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_MenstrualCramp); if (hediff != null && !Pawn.GetMenstruationComps().Any(comp => comp != this && comp.curStage == Stage.Bleeding)) Pawn.health.RemoveHediff(hediff); int totalFollicularHours = PeriodRandomizer(Stage.Follicular); // The total amount of time for both bleeding and follicular if (totalFollicularHours <= currentIntervalHours) // We've bled for so long that we completely missed the follicular phase GoOvulatoryStage(); else { currentIntervalHours = totalFollicularHours - currentIntervalHours; // I.e., the remaining follicular hours equals the total minus the bleeding hours elapsed GoNextStage(Stage.Follicular, false); } } else { if (curStageHrs < currentIntervalHours / 4) for (int i = 0; i < Configurations.CycleAcceleration; i++) BleedOut(); curStageHrs += Configurations.CycleAcceleration; StayCurrentStage(); } } protected virtual void PregnantAction() { if (!eggs.NullOrEmpty()) { FertilizationCheck(); EggDecay(); Implant(); } if (pregnancy != null && Pawn.health.hediffSet.hediffs.Contains(pregnancy)) { curStageHrs += 1; StayCurrentStageConst(Stage.Pregnant); } else { if (pregnancy != null) pregnancy = null; GoNextStage(Stage.Recover); } } protected virtual void RecoverAction() { if (curStageHrs >= currentIntervalHours) { if (ShouldBeInfertile()) { GoNextStage(Stage.Infertile); } else if (!IsBreedingSeason()) { GoNextStage(Stage.Anestrus); } else { GoNextStage(Stage.Follicular); } } else { curStageHrs += Configurations.CycleAcceleration; StayCurrentStage(); } } protected virtual void InfertileAction() { if (ShouldBeInfertile()) { StayCurrentStageConst(Stage.Infertile); } else { bool breedingSeason = IsBreedingSeason(); GoNextStage(breedingSeason ? Stage.Follicular : Stage.Anestrus, breedingSeason); } } protected virtual void AnestrusAction() { if (IsBreedingSeason()) { GoFollicularOrBleeding(); } else { StayCurrentStage(); } } protected virtual void ThoughtCumInside(Pawn cummer) { if (!xxx.is_human(Pawn) || !xxx.is_human(cummer)) return; if ((cummer.HasQuirk(QuirkUtility.Quirks.Teratophile) != (Pawn.GetStatValue(StatDefOf.PawnBeauty) >= 0)) || cummer.HasQuirk(QuirkUtility.Quirks.ImpregnationFetish) || cummer.HasQuirk(QuirkUtility.Quirks.Breeder)) { if (cummer.relations.OpinionOf(Pawn) <= -25) { cummer.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideM, Pawn); } else { cummer.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideM, Pawn); } } if (IsDangerDay) { if (Pawn.HasQuirk(QuirkUtility.Quirks.Breeder) || Pawn.HasQuirk(QuirkUtility.Quirks.ImpregnationFetish)) { Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideFFetish, cummer); } else if (Pawn.relations.OpinionOf(cummer) <= -5) { Pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(VariousDefOf.CameInsideF, cummer); Pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(VariousDefOf.HaterCameInsideFEstrus, cummer); Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideF, cummer); } else if (Pawn.IsInEstrus() && Pawn.relations.OpinionOf(cummer) < RJWHookupSettings.MinimumRelationshipToHookup) { Pawn.needs.mood.thoughts.memories.RemoveMemoriesOfDefWhereOtherPawnIs(VariousDefOf.CameInsideF, cummer); Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideFEstrus, cummer); } else if (!Pawn.relations.DirectRelationExists(PawnRelationDefOf.Spouse, cummer) && !Pawn.relations.DirectRelationExists(PawnRelationDefOf.Fiance, cummer)) { if (Pawn.health.capacities.GetLevel(xxx.reproduction) < 0.50f) Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideFLowFert, cummer); else Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideF, cummer); } } else { if (Pawn.HasQuirk(QuirkUtility.Quirks.Breeder) || Pawn.HasQuirk(QuirkUtility.Quirks.ImpregnationFetish)) { Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.CameInsideFFetishSafe, cummer); } else if (Pawn.relations.OpinionOf(cummer) <= -5) { Pawn.needs.mood.thoughts.memories.TryGainMemory(VariousDefOf.HaterCameInsideFSafe, cummer); } } } protected virtual void TaleCumInside(Pawn cummer) { // Only make the tale for human-on-human, consentual sex. Otherwise the art just gets too hard to phrase properly if (!xxx.is_human(Pawn) || !xxx.is_human(cummer) || Pawn == cummer) return; if (Pawn.CurJobDef != xxx.casual_sex && Pawn.CurJobDef != xxx.gettin_loved) return; if (!(Pawn.IsColonist || Pawn.IsPrisonerOfColony) && !(cummer.IsColonist || cummer.IsPrisonerOfColony)) return; if (!IsDangerDay) return; TaleRecorder.RecordTale(VariousDefOf.TaleCameInside, new object[] { cummer, Pawn }); } public void GoNextStage(Stage nextstage, bool calculateHours = true) { curStageHrs = 0; if (calculateHours) currentIntervalHours = PeriodRandomizer(nextstage); curStage = nextstage; } protected virtual void GoOvulatoryStage() { GoNextStage(Stage.Ovulatory); } //stage can be interrupted in other reasons protected void StayCurrentStage() { } //stage never changes protected void StayCurrentStageConst(Stage curstage) { } protected void GoFollicularOrBleeding() { if (Props.bleedingIntervalDays == 0 || noBleeding) { GoNextStage(Stage.Follicular); } else { GoNextStage(Stage.Bleeding); if (crampPain >= 0.05f) { AddCrampPain(); } } } protected int PeriodRandomizer(Stage stage) { float variabilityFactor = (EggHealth < 1.0f) ? 6.0f : 1.0f; // Most cycle lengthening or shortening occurs in the follicular phase, so weight towards that switch (stage) { case Stage.Follicular: return (int)(Props.follicularIntervalDays * 24 * (1 + Rand.Range(-cycleVariability, cycleVariability) * 1.5f * variabilityFactor) / (1 + (cycleSpeed - 1) * 1.5f)); case Stage.Luteal: return (int)(Props.lutealIntervalDays * 24 * (1 + Rand.Range(-cycleVariability, cycleVariability) * 0.5f * variabilityFactor) / (1 + (cycleSpeed - 1) * 0.5f)); case Stage.Bleeding: return (int)(Props.bleedingIntervalDays * 24 * (1 + Rand.Range(-cycleVariability, cycleVariability) * 0.5f * variabilityFactor) / (1 + (cycleSpeed - 1) * 0.5f)); case Stage.Recover: return (int)(Props.recoveryIntervalDays * 24 * Rand.Range(0.95f, 1.05f)); case Stage.Pregnant: return (int)MenstruationUtility.GestationHours(pregnancy); default: return 1; } } protected float InterspeciesImplantFactor(Pawn fertilizer) { if (fertilizer.def.defName == Pawn.def.defName) return 1.0f; else { if (RJWPregnancySettings.complex_interspecies) return SexUtility.BodySimilarity(Pawn, fertilizer); else return RJWPregnancySettings.interspecies_impregnation_modifier; } } protected float PainRandomizer() { float rand = Rand.Range(0.0f, 1.0f); if (rand < 0.01f) return Rand.Range(0.0f, 0.2f); else if (rand < 0.2f) return Rand.Range(0.1f, 0.2f); else if (rand < 0.8f) return Rand.Range(0.2f, 0.4f); else if (rand < 0.95f) return Rand.Range(0.4f, 0.6f); else return Rand.Range(0.6f, 1.0f); } protected Stage RandomStage() { Stage stage = Rand.ElementByWeight( Stage.Follicular, Props.follicularIntervalDays - Props.bleedingIntervalDays, Stage.Luteal, Props.lutealIntervalDays, Stage.Bleeding, Props.bleedingIntervalDays); switch (stage) { case Stage.Follicular: curStageHrs = Rand.Range(0, (Props.follicularIntervalDays - Props.bleedingIntervalDays) * 24); break; case Stage.Luteal: curStageHrs = Rand.Range(0, Props.lutealIntervalDays * 24); break; case Stage.Bleeding: curStageHrs = Rand.Range(0, Props.bleedingIntervalDays * 24); break; } return stage; } // Searches for a pregnancy unclaimed by any womb and put it in this one public void TakeLoosePregnancy() { if (pregnancy != null) return; IEnumerable 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)((float)RaceCyclesPerYear() * yearsWorth)); } public void RestoreEggs(float yearsWorth) { ovarypower += EggsRestoredPerBiosculptor(yearsWorth); } public class Egg : IExposable { public bool fertilized; public int lifespanhrs; public Pawn fertilizer; public int position; public int fertstage = 0; public Egg() { fertilized = false; lifespanhrs = (int)(96 * Configurations.EggLifespanMultiplier); fertilizer = null; position = 0; } public Egg(int lifespanhrs) { fertilized = false; this.lifespanhrs = (int)(lifespanhrs * Configurations.EggLifespanMultiplier); fertilizer = null; position = 0; } public void ExposeData() { Scribe_References.Look(ref fertilizer, "fertilizer", true); Scribe_Values.Look(ref fertilized, "fertilized", fertilized, true); Scribe_Values.Look(ref lifespanhrs, "lifespanhrs", lifespanhrs, true); Scribe_Values.Look(ref position, "position", position, true); Scribe_Values.Look(ref fertstage, "fertstage", fertstage, true); } } } public class HediffComp_Anus : HediffComp { protected float? originanussize; public float OriginAnusSize { get { if (originanussize == null) { originanussize = parent.Severity; } return originanussize ?? 0.1f; } } public override void CompExposeData() { base.CompExposeData(); Scribe_Values.Look(ref originanussize, "originanussize", originanussize, true); } public override void CompPostTick(ref float severityAdjustment) { } } }