using RimWorld; using RimWorld.Planet; using rjw; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using Verse; namespace RJW_Menstruation { public class CompProperties_Breast : HediffCompProperties { public static readonly ColorInt DefaultBlacknippleColor = new ColorInt(55, 20, 0); public string BreastTex = "Breasts/Breast"; public ColorInt BlacknippleColor = new ColorInt(55, 20, 0); public Color BlackNippleColor { get { return BlacknippleColor.ToColor; } } public CompProperties_Breast() { compClass = typeof(HediffComp_Breast); } } public class HediffComp_Breast : HediffComp { public const int tickInterval = GenDate.TicksPerHour * 3 / 2; public const float breastGrowthStart = 1f / 6f; public const float breastGrowthEnd = 1f / 3f; public static readonly SimpleCurve nippleTransitions = new SimpleCurve() { new CurvePoint(0f,0f), new CurvePoint(0.1f,0f), new CurvePoint(0.333f,0.167f), new CurvePoint(0.667f,0.833f), new CurvePoint(1.0f,1.0f) }; public const float nippleChange = 0.2f; public CompProperties_Breast Props; protected long ageOfLastBirth = 0; protected float maxBreastIncrement = -1f; protected float breastSizeIncreased = 0f; protected string debugGrowthStatus = "(Growth/shrink not yet calculated; run for 1.5h to update)"; protected float nippleProgress = 0f; protected float baseAlpha = -1f; // Will grow in response to pregnancy protected float baseAreola = -1f; protected float baseNipple = -1f; protected float cachedAlpha = -1f; // Calculated dynamically instead of saved protected float cachedAreola = -1f; // Actual size = these * breast size protected float cachedNipple = -1f; protected float babyHalfAge = -1f; protected Color cachedColor; protected bool loaded = false; protected float BabyHalfAge { get { if (babyHalfAge > 0f) return babyHalfAge; List ages = Pawn.def.race.lifeStageAges; if (ages?.Count > 1) babyHalfAge = ages[1].minAge / 2; if (babyHalfAge <= 0) babyHalfAge = 1.2f / 2; // Default to human if (RJWPregnancySettings.phantasy_pregnancy) babyHalfAge /= GenDate.DaysPerYear; return babyHalfAge; } } protected void ShrinkBreasts() { // The natural rate will take them from full to empty during the second half of their child's babyhood float shrinkRate = tickInterval * MaxBreastIncrement / (BabyHalfAge * GenDate.TicksPerYear); float shrinkAmount = Mathf.Min(shrinkRate, breastSizeIncreased); breastSizeIncreased -= shrinkAmount; parent.Severity -= shrinkAmount; } protected float MaxBreastIncrement { get { return maxBreastIncrement * Configurations.MaxBreastIncrementFactor; } } public Color NippleColor { get { return cachedColor; } } public float Alpha { get { return cachedAlpha; } } public float NippleSize { get { return cachedNipple * parent.Severity; } } public float AreolaSize { get { return cachedAreola * parent.Severity; } } public float BreastSizeIncreased { get { return breastSizeIncreased; } } public override void CompExposeData() { base.CompExposeData(); if (Scribe.mode == LoadSaveMode.LoadingVars) { // For compatibility Scribe_Values.Look(ref baseAlpha, "alphaPermanent", baseAlpha / 2, false); Scribe_Values.Look(ref baseAreola, "areolaSizePermanent", baseAreola / 2, false); Scribe_Values.Look(ref baseNipple, "nippleSizePermanent", baseNipple / 2, false); baseAlpha *= 2; baseAreola *= 2; baseNipple *= 2; } Scribe_Values.Look(ref ageOfLastBirth, "ageOfLastBirth", ageOfLastBirth, true); Scribe_Values.Look(ref maxBreastIncrement, "maxBreastIncrement", maxBreastIncrement, true); Scribe_Values.Look(ref breastSizeIncreased, "breastSizeIncreased", breastSizeIncreased, true); Scribe_Values.Look(ref nippleProgress, "nippleProgress", nippleProgress, true); Scribe_Values.Look(ref baseAlpha, "baseAlpha", baseAlpha, true); Scribe_Values.Look(ref baseAreola, "baseAreola", baseAreola, true); Scribe_Values.Look(ref baseNipple, "baseNipple", baseNipple, true); } public bool ShouldSimulate() { if (!Configurations.EnableAnimalCycle && Pawn.IsAnimal()) return false; if (Pawn.Spawned || Pawn.IsCaravanMember() || PawnUtility.IsTravelingInTransportPodWorldObject(Pawn)) return true; return false; } public override void CompPostTick(ref float severityAdjustment) { base.CompPostTick(ref severityAdjustment); // If an exception makes it out, RW will remove the hediff, so catch it here try { if ( !Pawn.IsHashIntervalTick(tickInterval) || !ShouldSimulate() ) { return; } CalculateBreastSize(); CalculateNipples(); UpdateNipples(); } catch (Exception ex) { Log.Error($"Error processing breasts of {Pawn}: {ex}"); } } public override void CompPostPostAdd(DamageInfo? dinfo) { if (!loaded) Initialize(); if (ageOfLastBirth > Pawn.ageTracker.AgeChronologicalTicks) ageOfLastBirth = CalculateLastBirth(); // catch transplant issues } public override void CompPostPostRemoved() { if (Pawn.health.hediffSet.hediffs.Contains(parent)) { Log.Warning($"Attempted to remove breast comp from wrong pawn ({Pawn})."); return; } base.CompPostPostRemoved(); } protected long CalculateLastBirth() { long youngestAge = (long)(BabyHalfAge * GenDate.TicksPerYear) * -2; // So a newborn isn't considered a new mother, either if ((Pawn.relations == null)) return youngestAge; bool hasChild = Pawn.relations.Children. Where(child => !Pawn.health.hediffSet.GetHediffs().Any(preg => preg.babies.Contains(child))). // no fetuses Where(child => child.GetMother() == Pawn). // not Dad TryMinBy(child => child.ageTracker.AgeBiologicalTicks, out Pawn youngest); if (hasChild) youngestAge = Pawn.ageTracker.AgeBiologicalTicks - youngest.ageTracker.AgeBiologicalTicks; return youngestAge; } public void Initialize() { Props = (CompProperties_Breast)props; if (maxBreastIncrement <= 0f) { maxBreastIncrement = Utility.RandGaussianLike(0.088f, 0.202f); } if (ageOfLastBirth == 0) { ageOfLastBirth = CalculateLastBirth(); } if (baseAlpha <= 0f) { baseAlpha = Utility.RandGaussianLike(0.0f, 0.3f) + Rand.Range(0.0f, 0.5f); } if (baseAreola <= 0f) { baseAreola = Utility.RandGaussianLike(0.0f, 1.0f); } if (baseNipple <= 0f) { baseNipple = Utility.RandGaussianLike(0.0f, 1.0f); } UpdateNipples(); loaded = true; } protected void CalculateBreastSize() { // Scenario A: the youngest child is less than halfway into babyhood: Full size if (ageOfLastBirth + BabyHalfAge * GenDate.TicksPerYear > Pawn.ageTracker.AgeBiologicalTicks) { debugGrowthStatus = "Full size due to young child"; if (breastSizeIncreased < MaxBreastIncrement) { parent.Severity += (MaxBreastIncrement - breastSizeIncreased); breastSizeIncreased = MaxBreastIncrement; } } // Scenario B: Pregnant, grow in the second half of first trimester else if (Pawn.IsRJWPregnant()) { float pregnancySize = Mathf.InverseLerp(breastGrowthStart, breastGrowthEnd, Pawn.GetFarthestPregnancyProgress()) * MaxBreastIncrement; if (breastSizeIncreased > pregnancySize) { debugGrowthStatus = "Shrinking due to being oversize for pregnancy"; // Breasts still large from the last kid ShrinkBreasts(); } else if (breastSizeIncreased < MaxBreastIncrement) { // Time to grow float growAmount = pregnancySize - breastSizeIncreased; if (growAmount != 0) debugGrowthStatus = "Growing due to pregnancy"; else debugGrowthStatus = "Pregnant, but not time to grow"; breastSizeIncreased += growAmount; parent.Severity += growAmount; } else debugGrowthStatus = "Pregnant and full size"; } // Scenario C: Not (or very early) pregnant and youngest child nonexistent or more than halfway into babyhood, time to shrink else if (breastSizeIncreased > 0) { debugGrowthStatus = "Shrinking due to no pregnancy nor young child"; ShrinkBreasts(); } else debugGrowthStatus = "Base size"; } protected void CalculateNipples() { float newNippleProgress; if (ageOfLastBirth + BabyHalfAge * GenDate.TicksPerYear > Pawn.ageTracker.AgeBiologicalTicks) newNippleProgress = 1f; else if (Pawn.IsRJWPregnant()) newNippleProgress = nippleTransitions.Evaluate(Pawn.GetFarthestPregnancyProgress()); else newNippleProgress = 0f; if (newNippleProgress < 0) newNippleProgress = 0; if (newNippleProgress == nippleProgress) return; // Nothing to change else if (newNippleProgress > nippleProgress) { float progressDifference = newNippleProgress - nippleProgress; // All nipple growth has a slight effect on the base // Not mathematically precise in hitting the goal at the end of the term, but close enough baseAlpha *= 1.0f + progressDifference * Configurations.PermanentNippleChange; if (baseAlpha > 1.0f) baseAlpha = 1.0f; baseAreola *= 1.0f + progressDifference * Configurations.PermanentNippleChange; if (baseAreola > 1.0f) baseAreola = 1.0f; baseNipple *= 1.0f + progressDifference * Configurations.PermanentNippleChange; if (baseNipple > 1.0f) baseNipple = 1.0f; nippleProgress = newNippleProgress; } else { nippleProgress -= tickInterval / (BabyHalfAge * GenDate.TicksPerYear); if (nippleProgress < newNippleProgress) nippleProgress = newNippleProgress; } } public void GaveBirth() { ageOfLastBirth = Pawn.ageTracker.AgeBiologicalTicks; } public void AdjustNippleProgress(float amount) { nippleProgress = Mathf.Clamp01(nippleProgress + amount); UpdateNipples(); } public void AdjustNippleSizeImmediately(float amount) { baseNipple = Mathf.Clamp01(baseNipple + amount); UpdateNipples(); } public void AdjustNippleColorImmediately(float amount) { baseAlpha = Mathf.Clamp01(baseAlpha + amount); UpdateNipples(); } public void AdjustAreolaSizeImmediately(float amount) { baseAreola = Mathf.Clamp01(baseAreola + amount); UpdateNipples(); } public void UpdateNipples() { cachedAlpha = baseAlpha + nippleProgress * nippleChange; cachedAreola = baseAreola + nippleProgress * nippleChange; cachedNipple = baseNipple + nippleProgress * nippleChange; // For some reason, Props can go null when RJW relocates the chest (e.g. some animals), so catch that cachedColor = Colors.CMYKLerp(Pawn.story?.SkinColor ?? Color.white, (Props?.BlackNippleColor ?? CompProperties_Breast.DefaultBlacknippleColor.ToColor), Alpha); } public void CopyBreastProperties(HediffComp_Breast original) { maxBreastIncrement = original.maxBreastIncrement; baseAlpha = original.baseAlpha; baseAreola = original.baseAreola; baseNipple = original.baseNipple; UpdateNipples(); } public string DebugInfo() { return "Size: " + parent.Severity + "\nIncrease: " + breastSizeIncreased + "\n" + debugGrowthStatus + "\nNipple progress: " + nippleProgress + "\nBase alpha: " + baseAlpha + "\nAlpha: " + cachedAlpha + "\nBase areola: " + baseAreola + "\nAreola: " + cachedAreola + "\nDisplayed areola: " + AreolaSize + "\nBase nipple: " + baseNipple + "\nNipple: " + cachedNipple + "\nDisplayed nipple: " + NippleSize; } } }