2006 lines
78 KiB
C#
2006 lines
78 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 ovulationIntervalHours = 12; //between the end of follicular and the egg emerging
|
|
public int lutealIntervalDays = 14; //after ovulation until bleeding
|
|
public int bleedingIntervalDays = 6; //must be less than folicularIntervalDays
|
|
public int recoveryIntervalDays = 10; //additional infertile days after gave birth
|
|
public int eggLifespanDays = 2; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday
|
|
public string wombTex = "Womb/Womb"; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday
|
|
public string vagTex = "Genitals/Vagina"; //fertiledays = ovaluationday - spermlifespan ~ ovaluationday + egglifespanday
|
|
public bool infertile = false;
|
|
public bool concealedEstrus = false;
|
|
public SeasonalBreed breedingSeason = SeasonalBreed.Always;
|
|
public int estrusDaysBeforeOvulation = 3;
|
|
public int eggMultiplier = 1;
|
|
|
|
|
|
public CompProperties_Menstruation()
|
|
{
|
|
|
|
compClass = typeof(HediffComp_Menstruation);
|
|
}
|
|
}
|
|
|
|
|
|
public class CompProperties_Anus : HediffCompProperties
|
|
{
|
|
public string analTex = "Genitals/Anal";
|
|
|
|
public CompProperties_Anus()
|
|
{
|
|
compClass = typeof(HediffComp_Anus);
|
|
}
|
|
}
|
|
|
|
public class HediffComp_Menstruation : HediffComp
|
|
{
|
|
const float minmakefilthvalue = 1.0f;
|
|
const int maxImplantDelayHours = 30 * GenDate.HoursPerDay;
|
|
const int minImplantAgeHours = 3 * GenDate.HoursPerDay;
|
|
const float fluidLeakThreshold = 3.5f;
|
|
const float pulloutSuccessRate = 0.8f;
|
|
const float fetishPulloutSuccessModifier = 0.25f;
|
|
|
|
public CompProperties_Menstruation Props;
|
|
public Stage curStage = Stage.Follicular;
|
|
public int curStageTicks = 0; // Actual number of ticks equals this / cycleAcceleration
|
|
private int tickInterval = -1;
|
|
const int recalculateTickInterval = GenTicks.TickLongInterval;
|
|
public bool loaded = false;
|
|
public bool initError = false;
|
|
public int ovarypower = -100000;
|
|
public int eggstack = 0;
|
|
public bool DoCleanWomb = false;
|
|
|
|
public enum Stage
|
|
{
|
|
Follicular,
|
|
Ovulatory,
|
|
Luteal,
|
|
Bleeding,
|
|
Pregnant,
|
|
Recover,
|
|
None,
|
|
Infertile,
|
|
Anestrus
|
|
}
|
|
|
|
public enum EstrusLevel
|
|
{
|
|
None,
|
|
Concealed,
|
|
Visible
|
|
}
|
|
|
|
public static readonly Dictionary<Stage, Texture2D> StageTexture = new Dictionary<Stage, Texture2D>()
|
|
{
|
|
{ Stage.Follicular, TextureCache.FollicularTexture },
|
|
{ Stage.Ovulatory, TextureCache.OvulatoryTexture },
|
|
{ Stage.Luteal, TextureCache.LutealTexture },
|
|
{ Stage.Bleeding, TextureCache.BleedingTexture },
|
|
{ Stage.Pregnant, TextureCache.PregnantTexture },
|
|
{ Stage.Recover, TextureCache.RecoverTexture }
|
|
};
|
|
|
|
|
|
protected List<Cum> cums;
|
|
protected List<Egg> eggs;
|
|
protected float cycleSpeed = -1;
|
|
protected float cycleVariability = -1;
|
|
protected int currentIntervalTicks = -1; // Actual number of ticks equals this / cycleAcceleration
|
|
protected float crampPain = -1;
|
|
protected float fluidToLeak = 0;
|
|
protected Need sexNeed = null;
|
|
protected string customwombtex = null;
|
|
protected string customvagtex = null;
|
|
protected bool estrusflag = false;
|
|
protected float? ovulationChanceCache = null; // Dirtied every simulation
|
|
protected float? implantationChanceCache = null;
|
|
protected int opcache = -1;
|
|
protected float antisperm = 0.0f;
|
|
// RJW pregnancy, or Biotech pregnancy/labor/laborpushing
|
|
protected Hediff pregnancy = null;
|
|
|
|
protected int eggLifeSpanTicks = 2 * GenDate.TicksPerDay;
|
|
protected EstrusLevel estrusLevel = EstrusLevel.Visible;
|
|
protected float ovulationFactor = 1f;
|
|
protected bool noBleeding = false;
|
|
|
|
private static readonly SimpleCurve SexFrequencyCurve = new SimpleCurve()
|
|
{
|
|
new CurvePoint(0.4f,0.05f),
|
|
new CurvePoint(0.6f,0.1f),
|
|
new CurvePoint(0.8f,0.25f),
|
|
new CurvePoint(1.0f,0.5f)
|
|
};
|
|
|
|
private static readonly SimpleCurve SexSatisfactionCurve = new SimpleCurve()
|
|
{
|
|
new CurvePoint(0.4f,0.5f),
|
|
new CurvePoint(0.6f,0.6f),
|
|
new CurvePoint(0.8f,0.7f),
|
|
new CurvePoint(1.0f,0.8f)
|
|
};
|
|
|
|
private static readonly SimpleCurve FertilityCurve = new SimpleCurve()
|
|
{
|
|
new CurvePoint(0.4f,0.01f),
|
|
new CurvePoint(0.6f,0.1f),
|
|
new CurvePoint(0.8f,0.25f),
|
|
new CurvePoint(1.0f,0.5f)
|
|
};
|
|
|
|
protected int TickInterval
|
|
{
|
|
get
|
|
{
|
|
if (tickInterval <= 0)
|
|
{
|
|
if (Pawn.IsAnimal()) tickInterval = Configurations.AnimalTickInterval;
|
|
else if (Pawn.IsColonist || Pawn.IsPrisonerOfColony || Pawn.IsSlaveOfColony) tickInterval = Configurations.ColonistTickInterval;
|
|
else tickInterval = Configurations.NonColonistTickInterval;
|
|
if (tickInterval < Configurations.TickIntervalMinimum) tickInterval = Configurations.TickIntervalMinimum;
|
|
else if (tickInterval > Configurations.TickIntervalMaximum) tickInterval = Configurations.TickIntervalMaximum;
|
|
}
|
|
return tickInterval;
|
|
}
|
|
set => tickInterval = value;
|
|
}
|
|
|
|
public float HoursBetweenSimulations => (float)TickInterval / GenDate.TicksPerHour;
|
|
|
|
public Hediff Pregnancy
|
|
{
|
|
get
|
|
{
|
|
if (pregnancy == null) return null;
|
|
else if (!Pawn.health.hediffSet.hediffs.Contains(pregnancy))
|
|
{
|
|
pregnancy = null;
|
|
return null;
|
|
}
|
|
else return pregnancy;
|
|
}
|
|
set => pregnancy = value;
|
|
}
|
|
|
|
public int OvaryPowerThreshold
|
|
{
|
|
get
|
|
{
|
|
if (opcache > 0) return opcache;
|
|
const float yearsBeforeMenopause = 6.0f;
|
|
opcache = (int)(RaceCyclesPerYear() *
|
|
AverageLitterSize() *
|
|
yearsBeforeMenopause *
|
|
(Pawn.RaceProps.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 => cums?.Sum(cum => cum.Volume) ?? 0;
|
|
}
|
|
public float TotalFertCum
|
|
{
|
|
get => cums?.Where(cum => CumCanFertilize(cum)).Sum(cum => cum.FertVolume) ?? 0;
|
|
}
|
|
public float TotalCumPercent
|
|
{
|
|
get => cums?.Sum(cum => cum.Volume) / Props.maxCumCapacity ?? 0;
|
|
}
|
|
public float CumCapacity
|
|
{
|
|
get
|
|
{
|
|
float res = Props.maxCumCapacity * Pawn.BodySize;
|
|
if (curStage != Stage.Pregnant || (pregnancy?.Severity ?? 0f) < 0.175f) res *= 500f;
|
|
return res;
|
|
}
|
|
}
|
|
//make follicular interval into half and double egg lifespan
|
|
public float CycleFactor
|
|
{
|
|
get
|
|
{
|
|
if (Pawn.IsBreeder()) return 0.5f;
|
|
|
|
return 1.0f;
|
|
}
|
|
}
|
|
|
|
// I hate doing this, but it's the least bad option
|
|
private bool calculatingOvulationChance = false;
|
|
public bool CalculatingOvulationChance { get => calculatingOvulationChance; }
|
|
|
|
private float CalculatedOvulationChance()
|
|
{
|
|
float ovulationChance = 1.0f;
|
|
if (EggHealth <= 0.0f) return 0.0f;
|
|
if (EggHealth < 1.0f / 3.0f) ovulationChance = 0.8f;
|
|
if (ModsConfig.BiotechActive && xxx.is_human(Pawn))
|
|
{
|
|
if (Pawn.SterileGenes()) return 0.0f;
|
|
if (!Pawn.RaceHasFertility()) return 0.0f;
|
|
if (AndroidsCompatibility.IsAndroid(Pawn) && parent.def != Genital_Helper.archotech_vagina) return 0.0f;
|
|
|
|
float ageFactor = 1.0f;
|
|
StatDefOf.Fertility.GetStatPart<StatPart_FertilityByGenderAge>()?.TransformValue(StatRequest.For(Pawn), ref ageFactor);
|
|
if (ageFactor <= 0.0f) return 0.0f; // Too young or too old
|
|
|
|
if (Pawn.IsBreeder()) ovulationChance *= 10.0f;
|
|
try
|
|
{
|
|
calculatingOvulationChance = true;
|
|
ovulationChance *= PawnCapacityUtility.CalculateCapacityLevel(Pawn.health.hediffSet, xxx.reproduction);
|
|
}
|
|
finally { calculatingOvulationChance = false; }
|
|
}
|
|
return ovulationChance;
|
|
}
|
|
|
|
private float CalculatedImplantChance()
|
|
{
|
|
if (ModsConfig.BiotechActive && xxx.is_human(Pawn))
|
|
{
|
|
// Implant factor will be based solely on pawn age, plus any rollover from ovulation chance
|
|
float factor = 1.0f;
|
|
StatDefOf.Fertility.GetStatPart<StatPart_FertilityByGenderAge>()?.TransformValue(StatRequest.For(Pawn), ref factor);
|
|
if (factor <= 0.0f) return 0.0f;
|
|
if (OvulationChance > 1.0f) factor *= OvulationChance;
|
|
return Props.baseImplantationChanceFactor * FertilityModifier * factor;
|
|
}
|
|
else
|
|
{
|
|
return Pawn.health.capacities.GetLevel(xxx.reproduction) * Props.baseImplantationChanceFactor * FertilityModifier * (Pawn.IsBreeder() ? 10.0f : 1.0f);
|
|
}
|
|
}
|
|
|
|
public float OvulationChance
|
|
{
|
|
get
|
|
{
|
|
if (ovulationChanceCache == null) ovulationChanceCache = CalculatedOvulationChance();
|
|
return ovulationChanceCache.Value;
|
|
}
|
|
}
|
|
|
|
// Before configuration setting
|
|
public float ImplantChance
|
|
{
|
|
get
|
|
{
|
|
if (implantationChanceCache == null) implantationChanceCache = CalculatedImplantChance();
|
|
return implantationChanceCache.Value;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<string> GetCumsInfo
|
|
{
|
|
get
|
|
{
|
|
if (cums.NullOrEmpty()) yield return Translations.Info_noCum;
|
|
else foreach (Cum cum in cums)
|
|
yield return string.Format("{0}: {1:0.##}ml", cum.notcum ? cum.notcumLabel : cum.pawn?.Label, cum.Volume);
|
|
}
|
|
}
|
|
public Color GetCumMixtureColor
|
|
{
|
|
get
|
|
{
|
|
Color mixedcolor = Color.white;
|
|
|
|
if (cums.NullOrEmpty()) return mixedcolor;
|
|
|
|
float mixedsofar = 0;
|
|
foreach (Cum cum in cums)
|
|
{
|
|
if (cum.Volume > 0)
|
|
{
|
|
mixedcolor = Colors.CMYKLerp(mixedcolor, cum.Color, cum.Volume / (mixedsofar + cum.Volume));
|
|
mixedsofar += cum.Volume;
|
|
}
|
|
}
|
|
return mixedcolor;
|
|
}
|
|
}
|
|
|
|
public Stage CurrentVisibleStage
|
|
{
|
|
get
|
|
{
|
|
if (curStage == Stage.Pregnant)
|
|
{
|
|
if (Configurations.InfoDetail == Configurations.DetailLevel.All || (pregnancy?.Visible ?? false))
|
|
return Stage.Pregnant;
|
|
else
|
|
return Stage.Luteal;
|
|
}
|
|
return curStage;
|
|
}
|
|
}
|
|
|
|
public string GetCurStageLabel
|
|
{
|
|
get
|
|
{
|
|
switch (CurrentVisibleStage)
|
|
{
|
|
case Stage.Follicular:
|
|
return Translations.Stage_Follicular + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
|
|
case Stage.Ovulatory:
|
|
return Translations.Stage_Ovulatory + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
|
|
case Stage.Luteal:
|
|
return Translations.Stage_Luteal + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
|
|
case Stage.Bleeding:
|
|
return Translations.Stage_Bleeding + (EggHealth < 1f ? " " + Translations.Stage_Climacteric : "");
|
|
case Stage.Pregnant:
|
|
return Translations.Stage_Pregnant;
|
|
case Stage.Recover:
|
|
return Translations.Stage_Recover;
|
|
case Stage.None:
|
|
case Stage.Infertile:
|
|
if (EggHealth <= 0f) return Translations.Stage_Menopause;
|
|
else return Translations.Stage_None;
|
|
case Stage.Anestrus:
|
|
return Translations.Stage_Anestrus;
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
}
|
|
public virtual string GetCurStageDesc
|
|
{
|
|
get
|
|
{
|
|
switch (CurrentVisibleStage)
|
|
{
|
|
case Stage.Follicular:
|
|
return Translations.Stage_Follicular_Desc + (EggHealth < 1f ? " " + Translations.Stage_Climacteric_Desc : "");
|
|
case Stage.Ovulatory:
|
|
return Translations.Stage_Ovulatory_Desc + (EggHealth < 1f ? " " + Translations.Stage_Climacteric_Desc : "");
|
|
case Stage.Luteal:
|
|
return Translations.Stage_Luteal_Desc + (EggHealth < 1f ? " " + Translations.Stage_Climacteric_Desc : "");
|
|
case Stage.Bleeding:
|
|
return Translations.Stage_Bleeding_Desc + (EggHealth < 1f ? " " + Translations.Stage_Climacteric_Desc : "");
|
|
case Stage.Pregnant:
|
|
return Translations.Stage_Pregnant_Desc;
|
|
case Stage.Recover:
|
|
return Translations.Stage_Recover_Desc;
|
|
case Stage.None:
|
|
case Stage.Infertile:
|
|
if (EggHealth <= 0f) return Translations.Stage_Menopause_Desc;
|
|
else return Translations.Stage_None_Desc;
|
|
case Stage.Anestrus:
|
|
return Translations.Stage_Anestrus_Desc;
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
}
|
|
|
|
public string WombTex
|
|
{
|
|
get => customwombtex ?? Props.wombTex;
|
|
set => customwombtex = value;
|
|
}
|
|
public string VagTex
|
|
{
|
|
get => customvagtex ?? Props.vagTex;
|
|
set => customvagtex = value;
|
|
}
|
|
public string GetFertilizingInfo
|
|
{
|
|
get
|
|
{
|
|
if (eggs.NullOrEmpty()) return "";
|
|
|
|
StringBuilder res = new StringBuilder();
|
|
int fertilized = eggs.Count(egg => egg.fertilized);
|
|
if (fertilized != 0) res.AppendFormat("{0} {1}", fertilized, Translations.Dialog_WombInfo05);
|
|
if (fertilized != 0 && eggs.Count - fertilized != 0) res.Append(", ");
|
|
if (cums.NullOrEmpty() || TotalFertCum == 0)
|
|
{
|
|
if (eggs.Count - fertilized != 0) res.AppendFormat("{0} {1}", eggs.Count - fertilized, Translations.Dialog_WombInfo07);
|
|
}
|
|
else
|
|
{
|
|
if (eggs.Count - fertilized != 0) res.AppendFormat("{0} {1}", eggs.Count - fertilized, Translations.Dialog_WombInfo06);
|
|
}
|
|
return res.ToString();
|
|
}
|
|
}
|
|
public bool IsEggFertilizing
|
|
{
|
|
get
|
|
{
|
|
if (eggs.NullOrEmpty()) return false;
|
|
return cums?.Any(cum => cum.FertVolume > 0) ?? false;
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// returns fertstage. if not fertilized returns -1
|
|
/// </summary>
|
|
public int EggFertilizedTime
|
|
{
|
|
get
|
|
{
|
|
if (eggs?.All(egg => !egg.fertilized) ?? true) return -1;
|
|
return eggs.Max(egg => egg.ticksSinceFertilization);
|
|
}
|
|
}
|
|
public IEnumerable<Pawn> GetCummersAndFertilizers()
|
|
{
|
|
if (!cums.NullOrEmpty())
|
|
foreach (Cum cum in cums)
|
|
yield return cum.pawn;
|
|
if (!eggs.NullOrEmpty())
|
|
foreach (Egg egg in eggs)
|
|
yield return egg.fertilizer;
|
|
}
|
|
public bool IsEggExist
|
|
{
|
|
get => !eggs.NullOrEmpty();
|
|
}
|
|
|
|
public int EggLifespanTicks
|
|
{
|
|
get => eggLifeSpanTicks;
|
|
}
|
|
|
|
public virtual bool IsDangerDay
|
|
{
|
|
get
|
|
{
|
|
if (Pawn.HasIUD()) return false;
|
|
|
|
switch (curStage)
|
|
{
|
|
case Stage.Follicular:
|
|
return curStageTicks > 0.7f * currentIntervalTicks;
|
|
case Stage.Ovulatory:
|
|
return true;
|
|
case Stage.Luteal:
|
|
return curStageTicks < EggLifespanTicks;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
public int GetNumOfEggs
|
|
{
|
|
get => eggs?.Count ?? 0;
|
|
}
|
|
public Color BloodColor
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
Color c = Pawn.RaceProps.BloodDef.graphicData.color;
|
|
return c;
|
|
}
|
|
catch
|
|
{
|
|
return Colors.blood;
|
|
}
|
|
}
|
|
}
|
|
|
|
public int CurStageIntervalTicks
|
|
{
|
|
get => currentIntervalTicks;
|
|
}
|
|
|
|
public float StageProgress
|
|
{
|
|
get
|
|
{
|
|
if (pregnancy == null) return Mathf.Clamp01((float)curStageTicks / currentIntervalTicks);
|
|
bool is_discovered = false;
|
|
switch (pregnancy)
|
|
{
|
|
case Hediff_BasePregnancy rjw_preg:
|
|
is_discovered = rjw_preg.is_discovered;
|
|
break;
|
|
case Hediff_Pregnant vanilla_preg:
|
|
is_discovered = vanilla_preg.Visible;
|
|
break;
|
|
case Hediff_Labor _:
|
|
case Hediff_LaborPushing _:
|
|
return 1.0f;
|
|
}
|
|
|
|
if (is_discovered || Configurations.infoDetail == Configurations.DetailLevel.All) return pregnancy.Severity;
|
|
|
|
// Luteal will appear to progress, hitting the end of the phase when the pregnancy is discovered
|
|
float discoveryTime = 0.5f;
|
|
if (Pawn.story?.bodyType == BodyTypeDefOf.Thin) discoveryTime = 0.25f;
|
|
else if (Pawn.story?.bodyType == BodyTypeDefOf.Female) discoveryTime = 0.35f;
|
|
// Estimated; there's no way to get the exact value after the fact without writing it into the save
|
|
float lutealProgressWhenImplanted = Math.Min(0.5f, maxImplantDelayHours / (Props.lutealIntervalDays * GenDate.HoursPerDay));
|
|
|
|
return GenMath.LerpDouble(0, discoveryTime, lutealProgressWhenImplanted, 1.0f, pregnancy.Severity);
|
|
}
|
|
}
|
|
|
|
public float StageProgessNextUpdate
|
|
{
|
|
get
|
|
{
|
|
if (pregnancy != null) return StageProgress;
|
|
return Mathf.Clamp01((float)(curStageTicks + TickInterval * Configurations.CycleAcceleration) / currentIntervalTicks);
|
|
}
|
|
}
|
|
|
|
public Texture2D GetStageTexture
|
|
{
|
|
get
|
|
{
|
|
if (!StageTexture.TryGetValue(CurrentVisibleStage, out Texture2D tex)) tex = TextureCache.PregnantTexture;
|
|
return tex;
|
|
}
|
|
}
|
|
|
|
public override void CompExposeData()
|
|
{
|
|
base.CompExposeData();
|
|
Scribe_Collections.Look(ref cums, saveDestroyedThings: true, label: "cums", lookMode: LookMode.Deep, ctorArgs: new object[0]);
|
|
Scribe_Collections.Look(ref eggs, saveDestroyedThings: true, label: "eggs", lookMode: LookMode.Deep, ctorArgs: new object[0]);
|
|
Scribe_Values.Look(ref curStage, "curStage", curStage, true);
|
|
Scribe_Values.Look(ref curStageTicks, "curStageTicks", curStageTicks, true);
|
|
Scribe_Values.Look(ref cycleSpeed, "cycleSpeed", cycleSpeed, true);
|
|
Scribe_Values.Look(ref cycleVariability, "cycleVariability", cycleVariability, true);
|
|
Scribe_Values.Look(ref currentIntervalTicks, "currentIntervalTicks", currentIntervalTicks, true);
|
|
Scribe_Values.Look(ref crampPain, "crampPain", crampPain, true);
|
|
Scribe_Values.Look(ref fluidToLeak, "fluidToLeak", 0);
|
|
Scribe_Values.Look(ref ovarypower, "ovarypower", ovarypower, true);
|
|
Scribe_Values.Look(ref eggstack, "eggstack", 0);
|
|
Scribe_Values.Look(ref estrusflag, "estrusflag", false);
|
|
Scribe_Values.Look(ref DoCleanWomb, "DoCleanWomb", false);
|
|
Scribe_References.Look(ref pregnancy, "pregnancy");
|
|
if (Scribe.mode == LoadSaveMode.PostLoadInit)
|
|
Initialize();
|
|
}
|
|
|
|
|
|
public override void CompPostPostAdd(DamageInfo? dinfo)
|
|
{
|
|
if (!loaded)
|
|
{
|
|
Initialize();
|
|
}
|
|
}
|
|
|
|
public void Notify_UpdatedGenes()
|
|
{
|
|
eggLifeSpanTicks = Props.eggLifespanDays * GenDate.TicksPerDay;
|
|
estrusLevel = Props.concealedEstrus ? EstrusLevel.Concealed : EstrusLevel.Visible;
|
|
ovulationFactor = 1f;
|
|
noBleeding = false;
|
|
opcache = -1;
|
|
|
|
if (Pawn.genes == null || !ModsConfig.BiotechActive) return;
|
|
foreach (MenstruationModExtension extension in Pawn.genes.GenesListForReading.Select(gene => gene.def.GetModExtension<MenstruationModExtension>()).Where(ext => ext != null))
|
|
{
|
|
eggLifeSpanTicks = (int)(eggLifeSpanTicks * extension.eggLifeTimeFactor);
|
|
if (extension.alwaysEstrus) estrusLevel = EstrusLevel.Visible;
|
|
else if (extension.neverEstrus) estrusLevel = EstrusLevel.None;
|
|
ovulationFactor *= extension.ovulationFactor;
|
|
if (extension.noBleeding) noBleeding = true;
|
|
}
|
|
if (eggLifeSpanTicks < 0) eggLifeSpanTicks = 0;
|
|
if (ovulationFactor < 0f) ovulationFactor = 0f;
|
|
}
|
|
|
|
public bool ShouldSimulate()
|
|
{
|
|
if (!Pawn.ShouldCycle()) return false;
|
|
if (Pawn.SpawnedOrAnyParentSpawned || Pawn.IsCaravanMember() || PawnUtility.IsTravelingInTransportPodWorldObject(Pawn)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool ShouldBeInfertile()
|
|
{
|
|
if (pregnancy != null) return false;
|
|
if (ImplantChance <= 0.0f) return true;
|
|
// Give the last egg ovulated a chance to implant
|
|
if (curStage == Stage.Luteal || curStage == Stage.Bleeding || curStage == Stage.Recover) return false;
|
|
if (EggHealth <= 0.0f) return true;
|
|
return false;
|
|
}
|
|
|
|
public override void CompPostTick(ref float severityAdjustment)
|
|
{
|
|
base.CompPostTick(ref severityAdjustment);
|
|
// If an exception makes it out, RW will remove the hediff, so catch it here
|
|
try
|
|
{
|
|
if (Pawn.IsHashIntervalTick(recalculateTickInterval)) TickInterval = -1; // Every so often, force TickInterval to be recalculated in case the pawn's status changed.
|
|
if (!Pawn.IsHashIntervalTick(TickInterval)) return;
|
|
|
|
if (!ShouldSimulate()) return;
|
|
|
|
// Initialize immediately if needed, but if there's an error, then don't spam it every tick
|
|
if (!loaded && !initError)
|
|
{
|
|
Log.Warning($"{Pawn}'s womb is ticking, but was not initialized first");
|
|
Initialize();
|
|
}
|
|
|
|
if (initError) Log.Warning($"Attempting to process {Pawn}'s womb uninitialized");
|
|
|
|
if (Pregnancy != null && curStage != Stage.Pregnant)
|
|
{
|
|
Log.Warning($"{Pawn}'s womb has a pregnancy, but was not in the pregnant stage");
|
|
GoNextStage(Stage.Pregnant);
|
|
}
|
|
|
|
BeforeSimulator();
|
|
|
|
switch (curStage)
|
|
{
|
|
case Stage.Follicular:
|
|
FollicularAction();
|
|
break;
|
|
case Stage.Ovulatory:
|
|
OvulatoryAction();
|
|
break;
|
|
case Stage.Luteal:
|
|
LutealAction();
|
|
break;
|
|
case Stage.Bleeding:
|
|
BleedingAction();
|
|
break;
|
|
case Stage.Pregnant:
|
|
PregnantAction();
|
|
break;
|
|
case Stage.Recover:
|
|
RecoverAction();
|
|
break;
|
|
case Stage.None:
|
|
break;
|
|
case Stage.Infertile:
|
|
InfertileAction();
|
|
break;
|
|
case Stage.Anestrus:
|
|
AnestrusAction();
|
|
break;
|
|
default:
|
|
GoNextStage(Stage.Follicular);
|
|
break;
|
|
}
|
|
|
|
AfterSimulator();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error($"Error processing womb of {Pawn}: {ex}");
|
|
}
|
|
}
|
|
|
|
public override void CompPostPostRemoved()
|
|
{
|
|
// If a hediff is removed from a pawn that does not have it, CompPostPostRemoved is still called on the pawn that does.
|
|
// If it was a legitimate removal, then it won't be in this pawn's hediff list anymore, as that removal occurs first
|
|
if (Pawn.health.hediffSet.hediffs.Contains(parent))
|
|
{
|
|
Log.Warning($"Attempted to remove menstruation comp from wrong pawn ({Pawn}).");
|
|
return;
|
|
}
|
|
switch (pregnancy)
|
|
{
|
|
case null:
|
|
case Hediff_MechanoidPregnancy _:
|
|
break;
|
|
case Hediff_BasePregnancy rjw_preg:
|
|
rjw_preg.Miscarry();
|
|
break;
|
|
case Hediff_Pregnant vanilla_preg:
|
|
Pawn.health.RemoveHediff(vanilla_preg);
|
|
break;
|
|
}
|
|
|
|
base.CompPostPostRemoved();
|
|
}
|
|
|
|
public override string CompTipStringExtra
|
|
{
|
|
get
|
|
{
|
|
if (Pawn.Dead) return null;
|
|
StringBuilder tip = new StringBuilder();
|
|
tip.Append(Translations.Dialog_WombInfo01);
|
|
tip.Append(": ");
|
|
tip.Append(GetCurStageLabel);
|
|
string fertInfo = GetFertilizingInfo;
|
|
if (CurrentVisibleStage == Stage.Luteal && fertInfo.Length > 0)
|
|
{
|
|
tip.AppendLine();
|
|
tip.Append(fertInfo);
|
|
}
|
|
return tip.ToString();
|
|
}
|
|
}
|
|
|
|
protected virtual int TicksToNextStage()
|
|
{
|
|
return Math.Max(0, (currentIntervalTicks - curStageTicks) / Configurations.CycleAcceleration);
|
|
}
|
|
|
|
public override string CompDebugString()
|
|
{
|
|
if (Pawn.Dead || curStage == Stage.None || curStage == Stage.Infertile || curStage == Stage.Pregnant) return null;
|
|
StringBuilder debugString = new StringBuilder();
|
|
debugString.Append($"Time to next state: ");
|
|
debugString.Append(GenDate.ToStringTicksToPeriod(TicksToNextStage()));
|
|
return debugString.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get fluid in womb that not a cum
|
|
/// </summary>
|
|
/// <param name="notcumlabel"></param>
|
|
/// <returns></returns>
|
|
public Cum GetNotCum(string notcumlabel)
|
|
{
|
|
return cums?.Find(cum => cum.notcum && cum.notcumLabel.Equals(notcumlabel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get pawn's cum in womb
|
|
/// </summary>
|
|
/// <param name="pawn"></param>
|
|
/// <returns></returns>
|
|
public Cum GetCum(Pawn pawn)
|
|
{
|
|
return cums?.Find(cum => !cum.notcum && cum.pawn == pawn);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inject pawn's cum into womb
|
|
/// </summary>
|
|
/// <param name="cummer"></param>
|
|
/// <param name="volume"></param>
|
|
/// <param name="fertility"></param>
|
|
/// <param name="precum"></param>
|
|
public void CumIn(Pawn cummer, float volume, float fertility = 1.0f, bool precum = false)
|
|
{
|
|
if (volume <= 0) return;
|
|
if (!precum && fertility > 0 && IsDangerDay && cummer.relations.GetPregnancyApproachForPartner(Pawn) == PregnancyApproach.AvoidPregnancy)
|
|
{
|
|
float successChance = pulloutSuccessRate;
|
|
if (cummer.HasImpregnationFetish()) successChance *= fetishPulloutSuccessModifier;
|
|
if (Pawn.HasImpregnationFetish()) successChance *= fetishPulloutSuccessModifier;
|
|
if (Rand.Chance(successChance)) return;
|
|
}
|
|
if (Pawn.HasIUD()) fertility /= 100f;
|
|
float cumd = TotalCumPercent;
|
|
float tmp = TotalCum + volume;
|
|
if (tmp > CumCapacity)
|
|
{
|
|
float cumoutrate = 1 - (CumCapacity / tmp);
|
|
bool merged = false;
|
|
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
|
|
{
|
|
if (cum.pawn?.Equals(cummer) ?? false)
|
|
{
|
|
cum.MergeWithCum(volume, fertility);
|
|
merged = true;
|
|
}
|
|
cum.DismishForce(cumoutrate);
|
|
}
|
|
if (!merged) cums.Add(new Cum(cummer, volume * (1 - cumoutrate), fertility));
|
|
}
|
|
else
|
|
{
|
|
|
|
bool merged = false;
|
|
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
|
|
{
|
|
if (cum.pawn?.Equals(cummer) ?? false)
|
|
{
|
|
cum.MergeWithCum(volume, fertility);
|
|
merged = true;
|
|
}
|
|
}
|
|
if (!merged) cums.Add(new Cum(cummer, volume, fertility));
|
|
}
|
|
cumd = TotalCumPercent - cumd;
|
|
|
|
if (!precum)
|
|
{
|
|
Pawn.records.AddTo(VariousDefOf.AmountofCreampied, volume);
|
|
AfterCumIn(cummer);
|
|
AfterFluidIn(cumd);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inject pawn's fluid into womb
|
|
/// </summary>
|
|
/// <param name="cummer"></param>
|
|
/// <param name="volume"></param>
|
|
/// <param name="notcumlabel"></param>
|
|
/// <param name="decayresist"></param>
|
|
/// <param name="filthdef"></param>
|
|
public void CumIn(Pawn cummer, float volume, string notcumlabel, float decayresist = 0, ThingDef filthdef = null)
|
|
{
|
|
if (volume <= 0) return;
|
|
float tmp = TotalCum + volume;
|
|
float cumd = TotalCumPercent;
|
|
if (tmp > CumCapacity)
|
|
{
|
|
float cumoutrate = 1 - (CumCapacity / tmp);
|
|
bool merged = false;
|
|
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
|
|
{
|
|
if (cum.notcum && (cum.pawn?.Equals(cummer) ?? false) && cum.notcumLabel.Equals(notcumlabel))
|
|
{
|
|
cum.MergeWithFluid(volume, decayresist, filthdef);
|
|
merged = true;
|
|
}
|
|
cum.DismishForce(cumoutrate);
|
|
}
|
|
if (!merged) cums.Add(new Cum(cummer, volume * (1 - cumoutrate), notcumlabel, decayresist, filthdef));
|
|
}
|
|
else
|
|
{
|
|
|
|
bool merged = false;
|
|
if (!cums.NullOrEmpty()) foreach (Cum cum in cums)
|
|
{
|
|
if (cum.notcum && (cum.pawn?.Equals(cummer) ?? false) && cum.notcumLabel.Equals(notcumlabel))
|
|
{
|
|
cum.MergeWithFluid(volume, decayresist, filthdef);
|
|
merged = true;
|
|
}
|
|
}
|
|
if (!merged) cums.Add(new Cum(cummer, volume, notcumlabel, decayresist, filthdef));
|
|
}
|
|
cumd = TotalCumPercent - cumd;
|
|
AfterNotCumIn();
|
|
AfterFluidIn(cumd);
|
|
}
|
|
|
|
protected virtual void AfterCumIn(Pawn cummer)
|
|
{
|
|
ThoughtCumInside(cummer);
|
|
TaleCumInside(cummer);
|
|
}
|
|
|
|
protected virtual void AfterNotCumIn()
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Action for both Cum and NotCum
|
|
/// </summary>
|
|
/// <param name="fd">Fluid deviation</param>
|
|
protected virtual void AfterFluidIn(float fd)
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
protected void BeforeCumOut(out Absorber absorber)
|
|
{
|
|
Hediff asa = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_ASA);
|
|
float asafactor = asa?.Severity ?? 0f;
|
|
|
|
if (Pawn.HasIUD()) antisperm = 0.70f + asafactor;
|
|
else antisperm = 0.0f + asafactor;
|
|
|
|
absorber = (Absorber)Pawn.apparel?.WornApparel.Find(x => x is Absorber);
|
|
if (absorber != null)
|
|
{
|
|
absorber.WearEffect(TickInterval);
|
|
if (absorber.dirty && absorber.EffectAfterDirty) absorber.DirtyEffect(TickInterval);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For natural leaking
|
|
/// </summary>
|
|
protected virtual void AfterCumOut(float amount, List<string> filthlabels)
|
|
{
|
|
fluidToLeak += amount;
|
|
if (fluidToLeak > fluidLeakThreshold) Pawn.needs?.mood?.thoughts?.memories?.TryGainMemory(VariousDefOf.LeakingFluids);
|
|
for (; fluidToLeak > fluidLeakThreshold; fluidToLeak -= fluidLeakThreshold)
|
|
{
|
|
if (cums.Count > 1) MakeCumFilthMixture(fluidLeakThreshold, filthlabels);
|
|
else if (cums.Count == 1) MakeCumFilth(cums.First(), fluidLeakThreshold);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For all type of leaking
|
|
/// </summary>
|
|
/// <param name="fd"></param>
|
|
protected virtual void AfterFluidOut(float fd)
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Excrete cums in womb naturally
|
|
/// </summary>
|
|
public void CumOut()
|
|
{
|
|
float leakfactor = 1.0f;
|
|
float totalleak = 0f;
|
|
float cumd = TotalCumPercent;
|
|
List<string> filthlabels = new List<string>();
|
|
BeforeCumOut(out Absorber absorber);
|
|
if (cums.NullOrEmpty()) return;
|
|
if (TotalCum > Props.maxCumCapacity * Pawn.BodySize) leakfactor = Math.Min(1 + (TotalCum - Props.maxCumCapacity * Pawn.BodySize) / 10, 2f);
|
|
if (absorber != null && absorber.dirty && !absorber.LeakAfterDirty) leakfactor = 0f;
|
|
if (Pawn.CurJobDef == xxx.knotted) leakfactor = 0f;
|
|
foreach (Cum cum in cums)
|
|
{
|
|
if (Rand.Chance(HoursBetweenSimulations)) cum.CumEffects(Pawn);
|
|
float vd = cum.DismishNatural(leakfactor, this, antisperm);
|
|
cum.MakeThinner((float)Configurations.CycleAcceleration * HoursBetweenSimulations);
|
|
totalleak += AbsorbCum(vd, absorber);
|
|
string tmp = "FilthLabelWithSource".Translate(cum.FilthDef.label, cum.pawn?.LabelShort ?? "Unknown", 1.ToString());
|
|
filthlabels.Add(tmp.Replace(" x1", ""));
|
|
}
|
|
AfterCumOut(totalleak, filthlabels);
|
|
cums.RemoveAll(cum => cum.ShouldRemove());
|
|
if (cums.NullOrEmpty()) fluidToLeak = 0;
|
|
cumd = TotalCumPercent - cumd;
|
|
AfterFluidOut(cumd);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force excrete cums in womb and get excreted amount of specific cum.
|
|
/// </summary>
|
|
/// <param name="targetcum"></param>
|
|
/// <param name="portion"></param>
|
|
/// <returns>Amount of target cum</returns>
|
|
public float CumOut(Cum targetcum, float portion = 0.1f)
|
|
{
|
|
if (cums.NullOrEmpty()) return 0;
|
|
float totalleak = 0;
|
|
List<string> filthlabels = new List<string>();
|
|
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));
|
|
if (cums.NullOrEmpty()) fluidToLeak = 0;
|
|
cumd = TotalCumPercent - cumd;
|
|
AfterFluidOut(cumd);
|
|
return outcum;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force excrete cums in womb and get mixture of cum.
|
|
/// </summary>
|
|
/// <param name="mixtureDef"></param>
|
|
/// <param name="portion"></param>
|
|
/// <returns></returns>
|
|
public CumMixture MixtureOut(ThingDef mixtureDef, float portion = 0.1f)
|
|
{
|
|
if (cums.NullOrEmpty()) return null;
|
|
Color color = GetCumMixtureColor;
|
|
float totalleak = 0;
|
|
List<string> cumlabels = new List<string>();
|
|
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));
|
|
if (cums.NullOrEmpty()) fluidToLeak = 0;
|
|
return new CumMixture(Pawn, totalleak, cumlabels, color, mixtureDef, pure);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Debug: Remove all cums from a womb
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public void RemoveAllCums()
|
|
{
|
|
cums.Clear();
|
|
}
|
|
|
|
public void RemoveAllEggs()
|
|
{
|
|
eggs.Clear();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Fertilize eggs and return the result
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected void FertilizationCheck()
|
|
{
|
|
if (eggs.NullOrEmpty()) return;
|
|
foreach (Egg egg in eggs)
|
|
{
|
|
if (!egg.fertilized) egg.fertilizer = Fertilize();
|
|
if (egg.fertilizer != null)
|
|
{
|
|
egg.fertilized = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
initError = true;
|
|
Props = (CompProperties_Menstruation)props;
|
|
|
|
if (cums == null) cums = new List<Cum>();
|
|
if (eggs == null) eggs = new List<Egg>();
|
|
|
|
PreInitialize();
|
|
|
|
if (Props.infertile)
|
|
{
|
|
curStage = Stage.None;
|
|
loaded = true;
|
|
initError = false;
|
|
return;
|
|
}
|
|
|
|
if (cycleSpeed < 0f) cycleSpeed = Utility.RandGaussianLike(0.8f, 1.2f);
|
|
if (cycleVariability < 0f) cycleVariability = MenstruationUtility.RandomVariabilityPercent();
|
|
|
|
InitOvary();
|
|
|
|
if (currentIntervalTicks < 0)
|
|
{
|
|
if (ShouldBeInfertile()) curStage = Stage.Infertile;
|
|
else if (!IsBreedingSeason()) curStage = Stage.Anestrus;
|
|
else curStage = RandomStage();
|
|
if (curStage == Stage.Follicular)
|
|
currentIntervalTicks = PeriodRandomizer(Stage.Follicular) - PeriodRandomizer(Stage.Bleeding);
|
|
else
|
|
currentIntervalTicks = PeriodRandomizer(curStage);
|
|
if (currentIntervalTicks < 0) currentIntervalTicks = 0;
|
|
else if (currentIntervalTicks < curStageTicks) curStageTicks = currentIntervalTicks;
|
|
}
|
|
if (crampPain < 0) crampPain = PainRandomizer();
|
|
PostInitialize();
|
|
|
|
TakeLoosePregnancy();
|
|
|
|
//Log.Message(Pawn.Label + " - Initialized menstruation comp");
|
|
loaded = true;
|
|
initError = false;
|
|
}
|
|
|
|
protected virtual void PreInitialize()
|
|
{
|
|
Notify_UpdatedGenes();
|
|
}
|
|
|
|
protected virtual void PostInitialize()
|
|
{
|
|
}
|
|
|
|
protected float AverageLitterSize()
|
|
{
|
|
float avglittersize;
|
|
try
|
|
{
|
|
avglittersize = Mathf.Max(Rand.ByCurveAverage(Pawn.RaceProps.litterSizeCurve), 1.0f);
|
|
}
|
|
catch (NullReferenceException)
|
|
{
|
|
avglittersize = 1.0f;
|
|
}
|
|
avglittersize *= ovulationFactor;
|
|
return avglittersize;
|
|
}
|
|
|
|
protected virtual float RaceCyclesPerYear()
|
|
{
|
|
int breedingSeasons = 0;
|
|
if (Props.breedingSeason == SeasonalBreed.Always) breedingSeasons = 4;
|
|
else
|
|
{
|
|
if ((Props.breedingSeason & SeasonalBreed.Spring) != 0) breedingSeasons++;
|
|
if ((Props.breedingSeason & SeasonalBreed.Summer) != 0) breedingSeasons++;
|
|
if ((Props.breedingSeason & SeasonalBreed.Fall) != 0) breedingSeasons++;
|
|
if ((Props.breedingSeason & SeasonalBreed.Winter) != 0) breedingSeasons++;
|
|
}
|
|
float breedingRatio = breedingSeasons / 4.0f;
|
|
return breedingRatio * GenDate.DaysPerYear / ((float)(Props.follicularIntervalDays + Props.lutealIntervalDays) / Configurations.CycleAccelerationDefault);
|
|
}
|
|
|
|
protected virtual int PawnEggsUsed(float pawnCyclesElapsed, float avglittersize)
|
|
{
|
|
return (int)(pawnCyclesElapsed * avglittersize);
|
|
}
|
|
|
|
public int GetOvaryPowerByAge()
|
|
{
|
|
float avglittersize = AverageLitterSize();
|
|
|
|
float fertStartAge = Pawn.RaceProps.lifeStageAges?.Find(stage => stage.def.reproductive)?.minAge ?? 0.0f;
|
|
float fertEndAge = Pawn.RaceProps.lifeExpectancy * (Pawn.IsAnimal() ? 0.96f : 0.58f); // RJW default fertility_endage for animal and humanlike
|
|
if (fertEndAge < fertStartAge) fertEndAge = fertStartAge;
|
|
|
|
float raceCyclesPerYear = RaceCyclesPerYear();
|
|
int lifetimeCycles = (int)(raceCyclesPerYear * (fertEndAge - fertStartAge));
|
|
int lifetimeEggs = (int)(lifetimeCycles * avglittersize * Props.eggMultiplier * Utility.RandGaussianLike(0.70f, 1.30f, 5));
|
|
|
|
float pawnCyclesPerYear = raceCyclesPerYear * cycleSpeed;
|
|
float pawnCyclesElapsed = Mathf.Max((Pawn.ageTracker.AgeBiologicalYearsFloat - fertStartAge) * pawnCyclesPerYear, 0.0f);
|
|
int pawnEggsUsed = PawnEggsUsed(pawnCyclesElapsed, avglittersize);
|
|
|
|
return Math.Max(lifetimeEggs - pawnEggsUsed, 0);
|
|
}
|
|
|
|
protected void InitOvary()
|
|
{
|
|
if (ovarypower < -50000)
|
|
{
|
|
ovarypower = GetOvaryPowerByAge();
|
|
if (Props.infertile) curStage = Stage.None;
|
|
else if (ovarypower < 1)
|
|
{
|
|
curStage = Stage.Infertile;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RecoverOvary(float multiply = 1.2f)
|
|
{
|
|
ovarypower = Math.Max(0, (int)(ovarypower * multiply));
|
|
}
|
|
|
|
protected virtual void BeforeSimulator()
|
|
{
|
|
ovulationChanceCache = null;
|
|
implantationChanceCache = null;
|
|
CumOut();
|
|
}
|
|
|
|
protected virtual void AfterSimulator()
|
|
{
|
|
if (ShouldBeInfertile())
|
|
{
|
|
eggs.Clear();
|
|
GoNextStage(Stage.Infertile);
|
|
}
|
|
if (EggHealth < 1f)
|
|
{
|
|
if (sexNeed == null) sexNeed = Pawn.needs.TryGetNeed(VariousDefOf.SexNeed);
|
|
if (sexNeed == null) return;
|
|
if (sexNeed.CurLevel < 0.5f) sexNeed.CurLevel += 0.01f * HoursBetweenSimulations / Math.Max(1, Pawn.GetMenstruationComps().Count());
|
|
}
|
|
}
|
|
|
|
protected virtual bool ShouldBeInEstrus()
|
|
{
|
|
if (!loaded)
|
|
Initialize();
|
|
switch (curStage)
|
|
{
|
|
case Stage.Follicular:
|
|
return curStageTicks > currentIntervalTicks - Props.estrusDaysBeforeOvulation * GenDate.TicksPerDay;
|
|
case Stage.Ovulatory:
|
|
return true;
|
|
case Stage.Luteal:
|
|
return curStageTicks < EggLifespanTicks;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public EstrusLevel GetEstrusLevel()
|
|
{
|
|
if (!ShouldBeInEstrus()) return EstrusLevel.None;
|
|
else return estrusLevel;
|
|
}
|
|
|
|
public void SetEstrus()
|
|
{
|
|
if (estrusLevel == EstrusLevel.None) return;
|
|
Hediff hediff = HediffMaker.MakeHediff(estrusLevel == EstrusLevel.Concealed ? VariousDefOf.Hediff_Estrus_Concealed : VariousDefOf.Hediff_Estrus, Pawn);
|
|
Pawn.health.AddHediff(hediff);
|
|
}
|
|
|
|
public bool IsBreedingSeason()
|
|
{
|
|
if (Props.breedingSeason == SeasonalBreed.Always) return true;
|
|
|
|
int tile = Pawn.Tile;
|
|
if (tile < 0) tile = Find.AnyPlayerHomeMap?.Tile ?? -1;
|
|
if (tile < 0) return true;
|
|
switch (GenLocalDate.Season(tile))
|
|
{
|
|
case Season.Spring:
|
|
return (Props.breedingSeason & SeasonalBreed.Spring) != 0;
|
|
case Season.Summer:
|
|
case Season.PermanentSummer:
|
|
return (Props.breedingSeason & SeasonalBreed.Summer) != 0;
|
|
case Season.Fall:
|
|
return (Props.breedingSeason & SeasonalBreed.Fall) != 0;
|
|
case Season.Winter:
|
|
case Season.PermanentWinter:
|
|
return (Props.breedingSeason & SeasonalBreed.Winter) != 0;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool CumCanFertilize(Cum cum)
|
|
{
|
|
return !cum.notcum &&
|
|
cum.FertVolume > 0 &&
|
|
!(cum.pawn?.Destroyed ?? true) &&
|
|
(RJWPregnancySettings.bestial_pregnancy_enabled || xxx.is_animal(Pawn) == xxx.is_animal(cum.pawn));
|
|
}
|
|
|
|
protected Pawn Fertilize()
|
|
{
|
|
if (cums.NullOrEmpty()) return null;
|
|
List<Cum> eligibleCum = cums.FindAll(cum => CumCanFertilize(cum));
|
|
if (eligibleCum.Count == 0) return null;
|
|
|
|
float totalFertPower = eligibleCum.Sum(cum => cum.FertVolume);
|
|
|
|
//float fertFailChancePerHour = Mathf.Pow(1.0f - Configurations.FertilizeChance, totalFertPower * Props.basefertilizationChanceFactor);
|
|
//float fertFailChancePerInterval = Mathf.Pow(fertFailChancePerHour, (float)TickInterval / GenDate.TicksPerHour);
|
|
float fertFailChancePerInterval = Mathf.Pow(1.0f - Configurations.FertilizeChance, totalFertPower * Props.basefertilizationChanceFactor * HoursBetweenSimulations);
|
|
|
|
if (Rand.Chance(fertFailChancePerInterval)) return null;
|
|
|
|
Pawn.records.AddTo(VariousDefOf.AmountofFertilizedEggs, 1);
|
|
float selection = Rand.Range(0.0f, totalFertPower);
|
|
|
|
foreach (Cum cum in eligibleCum)
|
|
{
|
|
selection -= cum.FertVolume;
|
|
if (selection <= 0) return cum.pawn;
|
|
}
|
|
|
|
// We shouldn't reach here, but floating point errors exist, so just to be sure, select whomever came the most
|
|
return eligibleCum.MaxBy(cum => cum.FertVolume).pawn;
|
|
}
|
|
|
|
|
|
protected bool Implant()
|
|
{
|
|
if (eggs.NullOrEmpty()) return false;
|
|
|
|
HashSet<Egg> deadeggs = new HashSet<Egg>();
|
|
bool pregnant = false;
|
|
foreach (Egg egg in eggs)
|
|
{
|
|
if (!egg.fertilized ||
|
|
egg.ticksSinceFertilization < minImplantAgeHours * GenDate.TicksPerHour ||
|
|
egg.ageTicks < Math.Min(Props.lutealIntervalDays * GenDate.TicksPerDay / 2, maxImplantDelayHours * GenDate.TicksPerHour))
|
|
continue;
|
|
else if (egg.fertilizer == null || egg.fertilizer.Destroyed)
|
|
{
|
|
if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to null or destroyed father");
|
|
deadeggs.Add(egg);
|
|
continue;
|
|
}
|
|
else if (Pawn.health.hediffSet.GetFirstHediff<Hediff_InsectEgg>() != null || pregnancy is Hediff_MechanoidPregnancy)
|
|
{
|
|
if (Configurations.Debug) Log.Message($"Could not implant {Pawn}'s egg due to insect or mechanoid pregnancy");
|
|
deadeggs.Add(egg);
|
|
continue;
|
|
}
|
|
else if (Rand.Chance(Configurations.ImplantationChance * ImplantChance * InterspeciesImplantFactor(egg.fertilizer)))
|
|
{
|
|
try
|
|
{
|
|
if (Configurations.Debug) Log.Message($"Implanting fertilized egg of {Pawn} into {parent}, father {egg.fertilizer}");
|
|
if (pregnancy == null)
|
|
{
|
|
Configurations.PregnancyType usePregnancy = xxx.is_human(Pawn) ? Configurations.PregnancySource : Configurations.PregnancyType.MultiplePregnancy;
|
|
switch (usePregnancy)
|
|
{
|
|
case Configurations.PregnancyType.BaseRJW:
|
|
|
|
if (Configurations.Debug) Log.Message($"Creating new base RJW pregnancy");
|
|
PregnancyHelper.AddPregnancyHediff(Pawn, egg.fertilizer);
|
|
// I hate having to do this, but it gets the newest pregnancy
|
|
List<Hediff_BasePregnancy> pregnancies = new List<Hediff_BasePregnancy>();
|
|
Pawn.health.hediffSet.GetHediffs(ref pregnancies);
|
|
pregnancy = pregnancies.MaxBy(hediff => hediff.loadID);
|
|
pregnant = true;
|
|
break;
|
|
|
|
case Configurations.PregnancyType.MultiplePregnancy:
|
|
if (Configurations.Debug) Log.Message($"Creating new menstruation pregnancy");
|
|
pregnancy = Hediff_BasePregnancy.Create<Hediff_MultiplePregnancy>(Pawn, egg.fertilizer);
|
|
pregnant = true;
|
|
deadeggs.Add(egg);
|
|
break;
|
|
|
|
case Configurations.PregnancyType.Biotech:
|
|
if (Configurations.Debug) Log.Message($"Creating new biotech pregnancy");
|
|
pregnancy = HediffMaker.MakeHediff(HediffDefOf.PregnantHuman, Pawn);
|
|
if (Configurations.EnableBiotechTwins)
|
|
pregnancy.TryGetComp<HediffComp_PregeneratedBabies>().AddNewBaby(Pawn, egg.fertilizer);
|
|
((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.ticksSinceFertilization / Configurations.CycleAcceleration;
|
|
rjw_preg.p_end_tick -= egg.ticksSinceFertilization / Configurations.CycleAcceleration;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (pregnancy)
|
|
{
|
|
case Hediff_Pregnant vanillaPreg: // Not going to do the labor ones
|
|
if (!Configurations.EnableBiotechTwins || !Configurations.EnableHeteroOvularTwins) goto default;
|
|
if (Configurations.Debug) Log.Message($"Adding to existing Biotech pregnancy {vanillaPreg.GetUniqueLoadID()}");
|
|
HediffComp_PregeneratedBabies comp = vanillaPreg.TryGetComp<HediffComp_PregeneratedBabies>();
|
|
if (comp == null) Log.Warning($"Trying to add Biotech egg to {Pawn}'s pregnancy without a pregenerated baby comp: {vanillaPreg.GetUniqueLoadID()}");
|
|
else
|
|
comp.AddNewBaby(Pawn, egg.fertilizer);
|
|
pregnant = true;
|
|
deadeggs.Add(egg);
|
|
break;
|
|
case Hediff_MultiplePregnancy multiPreg:
|
|
if (!Configurations.EnableHeteroOvularTwins) goto default;
|
|
if (Configurations.Debug) Log.Message($"Adding to existing pregnancy {multiPreg.GetUniqueLoadID()}");
|
|
multiPreg.AddNewBaby(Pawn, egg.fertilizer);
|
|
pregnant = true;
|
|
deadeggs.Add(egg);
|
|
break;
|
|
case Hediff_BasePregnancy _:
|
|
default:
|
|
if (Configurations.Debug) Log.Message($"Not adding to existing pregnancy {pregnancy?.GetUniqueLoadID()}");
|
|
pregnant = true;
|
|
deadeggs.Add(egg);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error($"Error creating pregnancy in {Pawn}'s womb, father {egg.fertilizer}: {ex}");
|
|
TakeLoosePregnancy();
|
|
deadeggs.Add(egg);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Configurations.Debug)
|
|
{
|
|
float interspeciesFactor = InterspeciesImplantFactor(egg.fertilizer);
|
|
float implantChance = Configurations.ImplantationChance * ImplantChance * interspeciesFactor;
|
|
Log.Message($"Fertilized egg of {Pawn} failed to implant (chance {implantChance.ToStringPercent()}, " +
|
|
(interspeciesFactor != 1.0f ? $"interspecies factor {interspeciesFactor.ToStringPercent()}, " : "") +
|
|
$"father {egg.fertilizer})");
|
|
}
|
|
deadeggs.Add(egg);
|
|
}
|
|
}
|
|
|
|
bool clearAllEggs = true;
|
|
switch (Configurations.PregnancySource)
|
|
{
|
|
case Configurations.PregnancyType.BaseRJW:
|
|
clearAllEggs = true;
|
|
break;
|
|
case Configurations.PregnancyType.MultiplePregnancy:
|
|
clearAllEggs = !Configurations.EnableHeteroOvularTwins;
|
|
break;
|
|
case Configurations.PregnancyType.Biotech:
|
|
clearAllEggs = !(Configurations.EnableBiotechTwins && Configurations.EnableHeteroOvularTwins);
|
|
break;
|
|
}
|
|
|
|
if (pregnant && clearAllEggs)
|
|
eggs.Clear();
|
|
else
|
|
eggs.RemoveAll(egg => deadeggs.Contains(egg));
|
|
return pregnant;
|
|
}
|
|
|
|
protected void BleedOut()
|
|
{
|
|
// ~1.5 per hour times acceleration
|
|
float bledAmount = Utility.VariationRange(0.03f * Configurations.BleedingAmount * Configurations.CycleAcceleration * HoursBetweenSimulations, 0.5f);
|
|
CumIn(Pawn, bledAmount, Translations.Menstrual_Blood, -5.0f, Pawn.RaceProps?.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.MapHeld == null) return;
|
|
if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.PositionHeld, Pawn.MapHeld, cum.FilthDef, cum.pawn?.LabelShort ?? "Unknown");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Absorb cum and return leaked amount
|
|
/// </summary>
|
|
/// <param name="amount"></param>
|
|
/// <param name="absorber"></param>
|
|
///
|
|
/// <returns></returns>
|
|
protected float AbsorbCum(float amount, Absorber absorber)
|
|
{
|
|
|
|
if (absorber == null)
|
|
{
|
|
//if (amount >= minmakefilthvalue) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort);
|
|
return amount;
|
|
}
|
|
|
|
float absorbable = absorber.GetStatValue(VariousDefOf.MaxAbsorbable);
|
|
absorber.SetColor(Colors.CMYKLerp(GetCumMixtureColor, absorber.DrawColor, 1f - amount / absorbable));
|
|
if (absorber.dirty)
|
|
{
|
|
//if (absorber.LeakAfterDirty) FilthMaker.TryMakeFilth(Pawn.Position, Pawn.Map, cum.FilthDef, cum.pawn.LabelShort);
|
|
return amount;
|
|
}
|
|
absorber.absorbedfluids += amount;
|
|
absorber.CheckDirty();
|
|
return 0;
|
|
}
|
|
|
|
protected float MakeCumFilthMixture(float amount, List<string> cumlabels)
|
|
{
|
|
if (Pawn.MapHeld == null) return 0;
|
|
if (amount >= minmakefilthvalue)
|
|
{
|
|
FilthMaker_Colored.TryMakeFilth(Pawn.PositionHeld, Pawn.MapHeld, VariousDefOf.FilthMixture, cumlabels, GetCumMixtureColor, false);
|
|
}
|
|
return amount;
|
|
}
|
|
|
|
protected void EggDecay()
|
|
{
|
|
HashSet<Egg> deadeggs = new HashSet<Egg>();
|
|
foreach (Egg egg in eggs)
|
|
{
|
|
egg.ageTicks += TickInterval * Configurations.CycleAcceleration;
|
|
if (egg.fertilized) egg.ticksSinceFertilization += TickInterval * Configurations.CycleAcceleration;
|
|
else
|
|
{
|
|
egg.lifeSpanTicks -= TickInterval * Configurations.CycleAcceleration;
|
|
if (egg.lifeSpanTicks < 0) deadeggs.Add(egg);
|
|
}
|
|
}
|
|
eggs.RemoveAll(egg => deadeggs.Contains(egg));
|
|
}
|
|
|
|
protected void AddCrampPain()
|
|
{
|
|
Hediff hediff = HediffMaker.MakeHediff(VariousDefOf.Hediff_MenstrualCramp, Pawn);
|
|
hediff.Severity = Utility.VariationRange(crampPain, 0.1f);
|
|
HediffCompProperties_SeverityPerDay Prop = (HediffCompProperties_SeverityPerDay)hediff.TryGetComp<HediffComp_SeverityPerDay>().props;
|
|
Prop.severityPerDay = -hediff.Severity / (currentIntervalTicks / GenDate.TicksPerDay) * Configurations.CycleAcceleration;
|
|
Pawn.health.AddHediff(hediff, parent.Part);
|
|
}
|
|
|
|
protected void AdvanceStageTime()
|
|
{
|
|
curStageTicks += TickInterval * Configurations.CycleAcceleration;
|
|
}
|
|
|
|
protected virtual void FollicularAction()
|
|
{
|
|
if (!IsBreedingSeason())
|
|
{
|
|
estrusflag = false;
|
|
GoNextStage(Stage.Anestrus);
|
|
return;
|
|
}
|
|
else if (curStageTicks >= currentIntervalTicks)
|
|
{
|
|
GoOvulatoryStage();
|
|
}
|
|
else
|
|
{
|
|
AdvanceStageTime();
|
|
if (!estrusflag && curStageTicks > currentIntervalTicks - Props.estrusDaysBeforeOvulation * GenDate.TicksPerDay)
|
|
{
|
|
estrusflag = true;
|
|
SetEstrus();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void OvulatoryAction()
|
|
{
|
|
if (curStageTicks < currentIntervalTicks)
|
|
{
|
|
AdvanceStageTime();
|
|
return;
|
|
}
|
|
estrusflag = false;
|
|
float eggnum;
|
|
try
|
|
{
|
|
eggnum = Math.Max(Rand.ByCurve(Pawn.RaceProps.litterSizeCurve), 1f);
|
|
}
|
|
catch (NullReferenceException)
|
|
{
|
|
eggnum = 1f;
|
|
}
|
|
catch (ArgumentException e)
|
|
{
|
|
Log.WarningOnce($"Invalid litterSizeCurve for {Pawn.def}: {e}", 642201874 + Pawn.thingIDNumber);
|
|
eggnum = 1f;
|
|
}
|
|
eggnum *= ovulationFactor;
|
|
int toOvulate = Math.Max(1, (int)eggnum + eggstack);
|
|
|
|
int ovulated = 0;
|
|
for (int i = 0; i < toOvulate; i++)
|
|
if (i < eggstack || Rand.Chance(OvulationChance)) // eggstack comes from drugs and are guaranteed ovulated
|
|
{
|
|
eggs.Add(new Egg((int)(EggLifespanTicks / CycleFactor)));
|
|
++ovulated;
|
|
}
|
|
ovarypower -= ovulated;
|
|
eggstack = 0;
|
|
if (Configurations.Debug && ovulated < toOvulate)
|
|
Log.Message($"{Pawn} ovulated {ovulated}/{toOvulate} eggs ({OvulationChance.ToStringPercent()} chance)");
|
|
|
|
GoNextStage(Stage.Luteal);
|
|
}
|
|
|
|
protected virtual void LutealAction()
|
|
{
|
|
if (curStageTicks >= currentIntervalTicks)
|
|
{
|
|
eggs.Clear();
|
|
if (EggHealth < 1f / 4f || (EggHealth < 1f / 3f && Rand.Chance(0.3f))) //skips bleeding
|
|
{
|
|
GoNextStage(Stage.Follicular);
|
|
}
|
|
else
|
|
{
|
|
GoFollicularOrBleeding();
|
|
}
|
|
}
|
|
else if (!eggs.NullOrEmpty())
|
|
{
|
|
FertilizationCheck();
|
|
EggDecay();
|
|
if (Implant())
|
|
{
|
|
GoNextStage(Stage.Pregnant);
|
|
}
|
|
else
|
|
{
|
|
AdvanceStageTime();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AdvanceStageTime();
|
|
}
|
|
|
|
}
|
|
|
|
protected virtual void BleedingAction()
|
|
{
|
|
if (curStageTicks >= currentIntervalTicks || noBleeding)
|
|
{
|
|
Hediff hediff = Pawn.health.hediffSet.GetFirstHediffOfDef(VariousDefOf.Hediff_MenstrualCramp);
|
|
if (hediff != null && !Pawn.GetMenstruationComps().Any(comp => comp != this && comp.curStage == Stage.Bleeding)) Pawn.health.RemoveHediff(hediff);
|
|
int totalFollicularTicks = PeriodRandomizer(Stage.Follicular); // The total amount of time for both bleeding and follicular
|
|
if (totalFollicularTicks <= currentIntervalTicks) // We've bled for so long that we completely missed the follicular phase
|
|
GoOvulatoryStage();
|
|
else
|
|
{
|
|
GoNextStage(Stage.Follicular, totalFollicularTicks - currentIntervalTicks); // I.e., the remaining follicular time equals the total minus the bleeding time elapsed
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (curStageTicks < currentIntervalTicks / 4) BleedOut();
|
|
AdvanceStageTime();
|
|
}
|
|
}
|
|
|
|
protected virtual void PregnantAction()
|
|
{
|
|
if (!eggs.NullOrEmpty())
|
|
{
|
|
FertilizationCheck();
|
|
EggDecay();
|
|
Implant();
|
|
}
|
|
|
|
if (Pregnancy != null)
|
|
curStageTicks += TickInterval;
|
|
else
|
|
GoNextStage(Stage.Recover);
|
|
}
|
|
|
|
protected virtual void RecoverAction()
|
|
{
|
|
if (curStageTicks >= currentIntervalTicks)
|
|
{
|
|
if (!IsBreedingSeason())
|
|
{
|
|
GoNextStage(Stage.Anestrus);
|
|
}
|
|
else
|
|
{
|
|
GoNextStage(Stage.Follicular);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AdvanceStageTime();
|
|
}
|
|
}
|
|
|
|
protected virtual void InfertileAction()
|
|
{
|
|
if (ShouldBeInfertile())
|
|
return;
|
|
else if (IsBreedingSeason())
|
|
GoNextStage(Stage.Follicular);
|
|
else
|
|
GoNextStage(Stage.Anestrus);
|
|
}
|
|
|
|
protected virtual void AnestrusAction()
|
|
{
|
|
if (IsBreedingSeason())
|
|
{
|
|
GoFollicularOrBleeding();
|
|
}
|
|
}
|
|
|
|
protected virtual void ThoughtCumInside(Pawn cummer)
|
|
{
|
|
if (!xxx.is_human(Pawn) || !xxx.is_human(cummer)) return;
|
|
|
|
MemoryThoughtHandler cummerMemories = cummer.needs.mood.thoughts.memories;
|
|
MemoryThoughtHandler pawnMemories = Pawn.needs.mood.thoughts.memories;
|
|
|
|
if (cummer.IsProPregnancy(out Precept preceptm) || (cummer.IsTeratophile() != (Pawn.GetStatValue(StatDefOf.PawnBeauty) >= 0)))
|
|
{
|
|
if (cummer.relations.OpinionOf(Pawn) <= -5)
|
|
cummerMemories.TryGainMemory(VariousDefOf.HaterCameInsideM, Pawn);
|
|
else if (preceptm != null)
|
|
cummerMemories.TryGainMemory(VariousDefOf.CameInsideMIdeo, Pawn, preceptm);
|
|
else
|
|
cummerMemories.TryGainMemory(VariousDefOf.CameInsideM, Pawn);
|
|
}
|
|
|
|
if (IsDangerDay)
|
|
{
|
|
if (Pawn.IsProPregnancy(out Precept preceptf))
|
|
{
|
|
if (preceptf != null)
|
|
{
|
|
if (Pawn.relations.OpinionOf(cummer) <= -5)
|
|
pawnMemories.TryGainMemory(VariousDefOf.HaterCameInsideFIdeo, cummer, preceptf);
|
|
else
|
|
pawnMemories.TryGainMemory(VariousDefOf.CameInsideFIdeo, cummer, preceptf);
|
|
}
|
|
else pawnMemories.TryGainMemory(VariousDefOf.CameInsideFFetish, cummer);
|
|
}
|
|
else if (Pawn.relations.OpinionOf(cummer) <= -5)
|
|
pawnMemories.TryGainMemory(VariousDefOf.HaterCameInsideF, cummer);
|
|
else if (Pawn.IsInEstrus() && Pawn.relations.OpinionOf(cummer) < RJWHookupSettings.MinimumRelationshipToHookup)
|
|
pawnMemories.TryGainMemory(VariousDefOf.HaterCameInsideFEstrus, cummer);
|
|
else if (!Pawn.relations.DirectRelationExists(PawnRelationDefOf.Spouse, cummer) && !Pawn.relations.DirectRelationExists(PawnRelationDefOf.Fiance, cummer))
|
|
{
|
|
if (Pawn.health.capacities.GetLevel(xxx.reproduction) < 0.50f) pawnMemories.TryGainMemory(VariousDefOf.CameInsideFLowFert, cummer);
|
|
else pawnMemories.TryGainMemory(VariousDefOf.CameInsideF, cummer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Pawn.IsProPregnancy(out Precept _))
|
|
pawnMemories.TryGainMemory(VariousDefOf.CameInsideFFetishSafe, cummer);
|
|
else if (Pawn.relations.OpinionOf(cummer) <= -5)
|
|
pawnMemories.TryGainMemory(VariousDefOf.HaterCameInsideFSafe, cummer);
|
|
}
|
|
}
|
|
|
|
protected virtual void TaleCumInside(Pawn cummer)
|
|
{
|
|
// Only make the tale for human-on-human, consentual sex. Otherwise the art just gets too hard to phrase properly
|
|
if (!xxx.is_human(Pawn) || !xxx.is_human(cummer) || Pawn == cummer) return;
|
|
if (Pawn.CurJobDef != xxx.casual_sex && Pawn.CurJobDef != xxx.gettin_loved) return;
|
|
if (!(Pawn.IsColonist || Pawn.IsPrisonerOfColony) && !(cummer.IsColonist || cummer.IsPrisonerOfColony)) return;
|
|
if (!IsDangerDay) return;
|
|
TaleRecorder.RecordTale(VariousDefOf.TaleCameInside, new object[] { cummer, Pawn });
|
|
}
|
|
|
|
public void GoNextStage(Stage nextstage, int? stageTicks = null)
|
|
{
|
|
curStageTicks = 0;
|
|
currentIntervalTicks = stageTicks ?? PeriodRandomizer(nextstage);
|
|
curStage = nextstage;
|
|
}
|
|
|
|
protected virtual void GoOvulatoryStage()
|
|
{
|
|
GoNextStage(Stage.Ovulatory);
|
|
}
|
|
|
|
protected void GoFollicularOrBleeding()
|
|
{
|
|
if (Props.bleedingIntervalDays == 0 || noBleeding)
|
|
{
|
|
GoNextStage(Stage.Follicular);
|
|
}
|
|
else
|
|
{
|
|
GoNextStage(Stage.Bleeding);
|
|
if (crampPain >= 0.05f)
|
|
{
|
|
AddCrampPain();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected int PeriodRandomizer(Stage stage)
|
|
{
|
|
float CycleSkew(float factor) => 1 + (cycleSpeed - 1) * factor;
|
|
|
|
float variabilityFactor = (EggHealth < 1.0f) ? 6.0f : 1.0f;
|
|
// Most cycle lengthening or shortening occurs in the follicular phase, so weight towards that
|
|
switch (stage)
|
|
{
|
|
case Stage.Follicular:
|
|
return (int)(Utility.VariationRange(Props.follicularIntervalDays * GenDate.TicksPerDay, cycleVariability * 1.5f * variabilityFactor) / CycleSkew(1.5f));
|
|
case Stage.Ovulatory:
|
|
return Props.ovulationIntervalHours * GenDate.TicksPerHour; // No variability for now
|
|
case Stage.Luteal:
|
|
return (int)(Utility.VariationRange(Props.lutealIntervalDays * GenDate.TicksPerDay, cycleVariability * 0.5f * variabilityFactor) / CycleSkew(0.5f));
|
|
case Stage.Bleeding:
|
|
return (int)(Utility.VariationRange(Props.bleedingIntervalDays * GenDate.TicksPerDay, cycleVariability * 0.5f * variabilityFactor) / CycleSkew(0.5f));
|
|
case Stage.Recover:
|
|
return (int)Utility.VariationRange(Props.recoveryIntervalDays * GenDate.TicksPerDay, 0.05f);
|
|
case Stage.Pregnant:
|
|
return (int)(MenstruationUtility.GestationHours(pregnancy) * GenDate.TicksPerHour);
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
protected float InterspeciesImplantFactor(Pawn fertilizer)
|
|
{
|
|
if (fertilizer.def.defName == Pawn.def.defName) return 1.0f;
|
|
else
|
|
{
|
|
if (RJWPregnancySettings.complex_interspecies) return SexUtility.BodySimilarity(Pawn, fertilizer);
|
|
else return RJWPregnancySettings.interspecies_impregnation_modifier;
|
|
}
|
|
}
|
|
|
|
protected float PainRandomizer()
|
|
{
|
|
float rand = Rand.Range(0.0f, 1.0f);
|
|
if (rand < 0.01f) return Rand.Range(0.0f, 0.2f);
|
|
else if (rand < 0.2f) return Rand.Range(0.1f, 0.2f);
|
|
else if (rand < 0.8f) return Rand.Range(0.2f, 0.4f);
|
|
else if (rand < 0.95f) return Rand.Range(0.4f, 0.6f);
|
|
else return Rand.Range(0.6f, 1.0f);
|
|
}
|
|
|
|
protected virtual float RandomOvulationChance => (float)Props.ovulationIntervalHours / GenDate.HoursPerDay;
|
|
|
|
protected Stage RandomStage()
|
|
{
|
|
Stage stage = Rand.ElementByWeight(
|
|
Stage.Follicular, Props.follicularIntervalDays - Props.bleedingIntervalDays,
|
|
Stage.Ovulatory, RandomOvulationChance,
|
|
Stage.Luteal, Props.lutealIntervalDays,
|
|
Stage.Bleeding, Props.bleedingIntervalDays);
|
|
|
|
switch (stage)
|
|
{
|
|
case Stage.Follicular:
|
|
curStageTicks = Rand.Range(0, (Props.follicularIntervalDays - Props.bleedingIntervalDays) * GenDate.TicksPerDay);
|
|
break;
|
|
case Stage.Ovulatory:
|
|
curStageTicks = Rand.Range(0, Props.ovulationIntervalHours * GenDate.TicksPerHour);
|
|
break;
|
|
case Stage.Luteal:
|
|
curStageTicks = Rand.Range(0, Props.lutealIntervalDays * GenDate.TicksPerDay);
|
|
break;
|
|
case Stage.Bleeding:
|
|
curStageTicks = Rand.Range(0, Props.bleedingIntervalDays * GenDate.TicksPerDay);
|
|
break;
|
|
}
|
|
return stage;
|
|
}
|
|
|
|
// Searches for a pregnancy unclaimed by any womb and put it in this one
|
|
public void TakeLoosePregnancy()
|
|
{
|
|
if (pregnancy != null) return;
|
|
|
|
IEnumerable<Hediff> pregnancies = Pawn.health.hediffSet.hediffs.Where(hediff =>
|
|
hediff is Hediff_BasePregnancy ||
|
|
hediff is Hediff_Pregnant ||
|
|
hediff is Hediff_Labor ||
|
|
hediff is Hediff_LaborPushing);
|
|
|
|
pregnancy =
|
|
pregnancies.Except(
|
|
Pawn.GetMenstruationComps().Select(comp => comp.pregnancy).Where(preg => preg != null)
|
|
).FirstOrDefault();
|
|
if (pregnancy != null)
|
|
GoNextStage(Stage.Pregnant);
|
|
}
|
|
|
|
public virtual void CopyCycleProperties(HediffComp_Menstruation original)
|
|
{
|
|
cycleSpeed = original.cycleSpeed;
|
|
cycleVariability = original.cycleVariability;
|
|
ovarypower = original.ovarypower;
|
|
crampPain = original.crampPain;
|
|
}
|
|
|
|
public int EggsRestoredPerBiosculptor(float yearsWorth)
|
|
{
|
|
return Math.Max(1, (int)(RaceCyclesPerYear() * yearsWorth * AverageLitterSize()));
|
|
}
|
|
|
|
public void RestoreEggs(float yearsWorth)
|
|
{
|
|
ovarypower += EggsRestoredPerBiosculptor(yearsWorth);
|
|
}
|
|
|
|
public class Egg : IExposable
|
|
{
|
|
public bool fertilized;
|
|
public int lifeSpanTicks; // Actual ticks scaled by cycleAcceleration
|
|
public Pawn fertilizer;
|
|
public int ageTicks;
|
|
public int ticksSinceFertilization = 0;
|
|
|
|
public Egg()
|
|
{
|
|
fertilized = false;
|
|
lifeSpanTicks = (int)(VariousDefOf.HumanVaginaCompProperties.eggLifespanDays * GenDate.TicksPerDay * Configurations.EggLifespanMultiplier);
|
|
fertilizer = null;
|
|
ageTicks = 0;
|
|
}
|
|
|
|
public Egg(int lifespanticks)
|
|
{
|
|
fertilized = false;
|
|
lifeSpanTicks = (int)(lifespanticks * Configurations.EggLifespanMultiplier);
|
|
fertilizer = null;
|
|
ageTicks = 0;
|
|
}
|
|
|
|
public void ExposeData()
|
|
{
|
|
Scribe_References.Look(ref fertilizer, "fertilizer", true);
|
|
Scribe_Values.Look(ref fertilized, "fertilized", false);
|
|
Scribe_Values.Look(ref lifeSpanTicks, "lifeSpanTicks", lifeSpanTicks, true);
|
|
Scribe_Values.Look(ref ageTicks, "ageTicks", ageTicks, true);
|
|
Scribe_Values.Look(ref ticksSinceFertilization, "ticksSinceFertilization", 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class HediffComp_Anus : HediffComp
|
|
{
|
|
public CompProperties_Anus Props => (CompProperties_Anus)props;
|
|
}
|
|
}
|