rjw_menstruation/1.4/source/RJW_Menstruation/RJW_Menstruation/HediffComps/HediffComp_Menstruation.cs

2017 lines
76 KiB
C#

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<Stage, Texture2D> StageTexture = new Dictionary<Stage, Texture2D>()
{
{ Stage.Follicular, TextureCache.FollicularTexture },
{ 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 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<StatPart_FertilityByGenderAge>();
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<string> GetCumsInfo
{
get
{
if (cums.NullOrEmpty()) yield return Translations.Info_noCum;
else foreach (Cum cum in cums)
{
if (!cum.notcum) yield return string.Format("{0}: {1:0.##}ml", cum.pawn?.Label, cum.Volume);
else yield return string.Format("{0}: {1:0.##}ml", cum.notcumLabel, cum.Volume);
}
}
}
public Color GetCumMixtureColor
{
get
{
Color mixedcolor = Color.white;
if (cums.NullOrEmpty()) return mixedcolor;
float mixedsofar = 0;
foreach (Cum cum in cums)
{
if (cum.Volume > 0)
{
mixedcolor = Colors.CMYKLerp(mixedcolor, cum.Color, cum.Volume / (mixedsofar + cum.Volume));
mixedsofar += cum.Volume;
}
}
return mixedcolor;
}
}
public Stage CurrentVisibleStage
{
get
{
if (curStage == Stage.Pregnant)
{
if (Configurations.InfoDetail == Configurations.DetailLevel.All || (pregnancy?.Visible ?? false))
return Stage.Pregnant;
else
return Stage.Luteal;
}
return curStage;
}
}
public string GetCurStageLabel
{
get
{
switch (CurrentVisibleStage)
{
case Stage.Follicular:
return Translations.Stage_Follicular + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
case Stage.Ovulatory:
return Translations.Stage_Ovulatory + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
case Stage.Luteal:
return Translations.Stage_Luteal + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
case Stage.Bleeding:
return Translations.Stage_Bleeding + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
case Stage.Pregnant:
return Translations.Stage_Pregnant;
case Stage.Recover:
return Translations.Stage_Recover;
case Stage.None:
case Stage.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;
}
}
/// <summary>
/// returns fertstage. if not fertilized returns -1
/// </summary>
public int IsFertilized
{
get
{
if (eggs?.All(egg => !egg.fertilized) ?? true) return -1;
return eggs.Max(egg => egg.fertstage);
}
}
public IEnumerable<Pawn> GetCummersAndFertilizers()
{
if (!cums.NullOrEmpty())
foreach (Cum cum in cums)
yield return cum.pawn;
if (!eggs.NullOrEmpty())
foreach (Egg egg in eggs)
yield return egg.fertilizer;
}
public bool IsEggExist
{
get
{
return !eggs.NullOrEmpty();
}
}
public 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();
}
/// <summary>
/// Get fluid in womb that not a cum
/// </summary>
/// <param name="notcumlabel"></param>
/// <returns></returns>
public Cum GetNotCum(string notcumlabel)
{
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
{
if (cum.notcum && cum.notcumLabel.Equals(notcumlabel)) return cum;
}
return null;
}
/// <summary>
/// Get pawn's cum in womb
/// </summary>
/// <param name="pawn"></param>
/// <returns></returns>
public Cum GetCum(Pawn pawn)
{
return cums?.Find(cum => !cum.notcum && cum.pawn == pawn);
}
/// <summary>
/// Inject pawn's cum into womb
/// </summary>
/// <param name="pawn"></param>
/// <param name="volume"></param>
/// <param name="fertility"></param>
/// <param name="precum"></param>
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);
}
}
/// <summary>
/// Inject pawn's fluid into womb
/// </summary>
/// <param name="pawn"></param>
/// <param name="volume"></param>
/// <param name="notcumlabel"></param>
/// <param name="decayresist"></param>
/// <param name="filthdef"></param>
public void CumIn(Pawn pawn, float volume, string notcumlabel, float decayresist = 0, ThingDef filthdef = null)
{
if (volume <= 0) return;
float tmp = TotalCum + volume;
float cumd = TotalCumPercent;
if (tmp > CumCapacity)
{
float cumoutrate = 1 - (CumCapacity / tmp);
bool merged = false;
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
{
if (cum.notcum && cum.pawn.Equals(pawn) && cum.notcumLabel.Equals(notcumlabel))
{
cum.MergeWithFluid(volume, decayresist, filthdef);
merged = true;
}
cum.DismishForce(cumoutrate);
}
if (!merged) cums.Add(new Cum(pawn, volume * (1 - cumoutrate), notcumlabel, decayresist, filthdef));
}
else
{
bool merged = false;
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
{
if (cum.notcum && cum.pawn.Equals(pawn) && cum.notcumLabel.Equals(notcumlabel))
{
cum.MergeWithFluid(volume, decayresist, filthdef);
merged = true;
}
}
if (!merged) cums.Add(new Cum(pawn, volume, notcumlabel, decayresist, filthdef));
}
cumd = TotalCumPercent - cumd;
AfterNotCumIn();
AfterFluidIn(cumd);
}
protected virtual void AfterCumIn(Pawn cummer)
{
ThoughtCumInside(cummer);
TaleCumInside(cummer);
}
protected virtual void AfterNotCumIn()
{
}
/// <summary>
/// Action for both Cum and NotCum
/// </summary>
/// <param name="fd">Fluid deviation</param>
protected virtual void AfterFluidIn(float fd)
{
}
protected void BeforeCumOut(out Absorber absorber)
{
Hediff asa = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_ASA);
float asafactor = asa?.Severity ?? 0f;
if (Pawn.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();
}
}
/// <summary>
/// For natural leaking
/// </summary>
protected virtual void AfterCumOut()
{
Pawn.needs?.mood?.thoughts?.memories?.TryGainMemory(VariousDefOf.LeakingFluids);
}
/// <summary>
/// For all type of leaking
/// </summary>
/// <param name="fd"></param>
protected virtual void AfterFluidOut(float fd)
{
}
/// <summary>
/// Excrete cums in womb naturally
/// </summary>
public void CumOut()
{
float leakfactor = 1.0f;
float totalleak = 0f;
float cumd = TotalCumPercent;
List<string> filthlabels = new List<string>();
BeforeCumOut(out Absorber absorber);
if (cums.NullOrEmpty()) return;
if (TotalCum > Props.maxCumCapacity * Pawn.BodySize) leakfactor = Math.Min(1 + (TotalCum - Props.maxCumCapacity * Pawn.BodySize) / 10, 2f);
if (absorber != null && absorber.dirty && !absorber.LeakAfterDirty) leakfactor = 0f;
if (Pawn.CurJobDef == xxx.knotted) leakfactor = 0f;
HashSet<Cum> removecums = new HashSet<Cum>();
foreach (Cum cum in cums)
{
cum.CumEffects(Pawn);
float vd = cum.DismishNatural(leakfactor, this, antisperm);
cum.MakeThinner(Configurations.CycleAcceleration);
totalleak += AbsorbCum(vd, absorber);
string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
filthlabels.Add(tmp.Replace(" x1", ""));
if (cum.ShouldRemove()) removecums.Add(cum);
}
if (cums.Count > 1) MakeCumFilthMixture(totalleak, filthlabels);
else if (cums.Count == 1) MakeCumFilth(cums.First(), totalleak);
cums.RemoveAll(cum => removecums.Contains(cum));
cumd = TotalCumPercent - cumd;
if (totalleak >= 1.0f) AfterCumOut();
AfterFluidOut(cumd);
}
/// <summary>
/// Force excrete cums in womb and get excreted amount of specific cum.
/// </summary>
/// <param name="targetcum"></param>
/// <param name="portion"></param>
/// <returns>Amount of target cum</returns>
public float CumOut(Cum targetcum, float portion = 0.1f)
{
if (cums.NullOrEmpty()) return 0;
float totalleak = 0;
List<string> filthlabels = new List<string>();
float outcum = 0;
float cumd = TotalCumPercent;
HashSet<Cum> removecums = new HashSet<Cum>();
foreach (Cum cum in cums)
{
float vd = cum.DismishForce(portion);
if (cum.Equals(targetcum)) outcum = vd;
//MakeCumFilth(cum, vd - cum.volume);
string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
filthlabels.Add(tmp.Replace(" x1", ""));
totalleak += vd;
if (cum.ShouldRemove()) removecums.Add(cum);
}
if (cums.Count > 1) MakeCumFilthMixture(totalleak, filthlabels);
else if (cums.Count == 1) MakeCumFilth(cums.First(), totalleak);
cums.RemoveAll(cum => removecums.Contains(cum));
cumd = TotalCumPercent - cumd;
AfterFluidOut(cumd);
return outcum;
}
/// <summary>
/// Force excrete cums in womb and get mixture of cum.
/// </summary>
/// <param name="mixtureDef"></param>
/// <param name="portion"></param>
/// <returns></returns>
public CumMixture MixtureOut(ThingDef mixtureDef, float portion = 0.1f)
{
if (cums.NullOrEmpty()) return null;
Color color = GetCumMixtureColor;
float totalleak = 0;
List<string> cumlabels = new List<string>();
HashSet<Cum> removecums = new HashSet<Cum>();
bool pure = true;
foreach (Cum cum in cums)
{
float vd = cum.DismishForce(portion);
string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
cumlabels.Add(tmp.Replace(" x1", ""));
totalleak += vd;
if (cum.ShouldRemove()) removecums.Add(cum);
if (cum.notcum) pure = false;
}
cums.RemoveAll(cum => removecums.Contains(cum));
return new CumMixture(Pawn, totalleak, cumlabels, color, mixtureDef, pure);
}
/// <summary>
/// Debug: Remove all cums from a womb
/// </summary>
/// <returns></returns>
public void RemoveAllCums()
{
cums.Clear();
}
/// <summary>
/// Fertilize eggs and return the result
/// </summary>
/// <returns></returns>
protected void FertilizationCheck()
{
if (eggs.NullOrEmpty()) return;
foreach (Egg egg in eggs)
{
if (!egg.fertilized) egg.fertilizer = Fertilize();
if (egg.fertilizer != null)
{
egg.fertilized = true;
}
}
}
public void Initialize()
{
initError = true;
Props = (CompProperties_Menstruation)props;
Notify_UpdatedGenes();
if (Props.infertile)
{
if (cums == null) cums = new List<Cum>();
curStage = Stage.None;
loaded = true;
initError = false;
return;
}
if (cycleSpeed < 0f) cycleSpeed = Utility.RandGaussianLike(0.8f, 1.2f);
if (cycleVariability < 0f) cycleVariability = MenstruationUtility.RandomVariabilityPercent();
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<Cum>();
if (eggs == null) eggs = new List<Egg>();
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<Cum> eligibleCum = cums.FindAll(cum => !cum.notcum && cum.FertVolume > 0 && cum.pawn != null && (RJWPregnancySettings.bestial_pregnancy_enabled || xxx.is_animal(Pawn) == xxx.is_animal(cum.pawn)));
if (eligibleCum.Count == 0) return null;
float totalFertPower = eligibleCum.Sum(cum => cum.FertVolume);
if (Rand.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<Egg> deadeggs = new HashSet<Egg>();
bool pregnant = false;
foreach (Egg egg in eggs)
{
if (!egg.fertilized ||
egg.fertstage < minImplantAgeHours ||
egg.position < Math.Min(Props.lutealIntervalDays * 24 / 2, maxImplantDelayHours))
continue;
else if (egg.fertilizer == null)
{
if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to null father");
deadeggs.Add(egg);
continue;
}
else if (Pawn.health.hediffSet.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 (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<HediffComp_PregeneratedBabies>();
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<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);
((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;
}
/// <summary>
/// Make filth ignoring absorber
/// </summary>
/// <param name="cum"></param>
/// <param name="amount"></param>
protected void MakeCumFilth(Cum cum, float amount)
{
if (Pawn.Map == null) return;
if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn?.LabelShort ?? "Unknown");
}
/// <summary>
/// Absorb cum and return leaked amount
/// </summary>
/// <param name="amount"></param>
/// <param name="absorber"></param>
///
/// <returns></returns>
protected float AbsorbCum(float amount, Absorber absorber)
{
if (absorber == null)
{
//if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort);
return amount;
}
float absorbable = absorber.GetStatValue(VariousDefOf.MaxAbsorbable);
absorber.SetColor(Colors.CMYKLerp(GetCumMixtureColor, absorber.DrawColor, 1f - amount / absorbable));
if (absorber.dirty)
{
//if (absorber.LeakAfterDirty) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort);
return amount;
}
absorber.absorbedfluids += amount;
if (absorber.absorbedfluids > absorbable && !Pawn.apparel.IsLocked(absorber))
{
absorber.def = absorber.DirtyDef;
//absorber.fluidColor = GetCumMixtureColor;
absorber.dirty = true;
}
return 0;
}
protected float MakeCumFilthMixture(float amount, List<string> cumlabels)
{
if (Pawn.Map == null) return 0;
if (amount >= minmakefilthvalue)
{
FilthMaker_Colored.TryMakeFilth(Pawn.Position, Pawn.Map, VariousDefOf.FilthMixture, cumlabels, GetCumMixtureColor, false);
}
return amount;
}
protected void EggDecay()
{
HashSet<Egg> deadeggs = new HashSet<Egg>();
foreach (Egg egg in eggs)
{
egg.position += Configurations.CycleAcceleration;
if (egg.fertilized) egg.fertstage += Configurations.CycleAcceleration;
else
{
egg.lifespanhrs -= Configurations.CycleAcceleration;
if (egg.lifespanhrs < 0) deadeggs.Add(egg);
}
}
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<HediffComp_SeverityPerDay>().props;
Prop.severityPerDay = -hediff.Severity / (currentIntervalHours / 24) * Configurations.CycleAcceleration;
Pawn.health.AddHediff(hediff, Genital_Helper.get_genitalsBPR(Pawn));
}
protected virtual void FollicularAction()
{
if (!IsBreedingSeason())
{
estrusflag = false;
GoNextStage(Stage.Anestrus);
return;
}
else if (curStageHrs >= currentIntervalHours)
{
GoOvulatoryStage();
}
else
{
curStageHrs += Configurations.CycleAcceleration;
if (!estrusflag && curStageHrs > currentIntervalHours - Props.estrusDaysBeforeOvulation * 24)
{
estrusflag = true;
SetEstrus();
}
StayCurrentStage();
}
}
protected virtual void OvulatoryAction()
{
estrusflag = false;
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<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)((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)
{
}
}
}