code:
// #define TESTMODE // Uncomment to enable logging.

using Verse;
using Verse.AI;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using System.Diagnostics;
using System;
using UnityEngine;
using Verse.AI.Group;

namespace rjw
{
	/// <summary>
	/// Helper for whoring related stuff
	/// </summary>
	class WhoringHelper
	{
		private static readonly SimpleCurve whoring_age_curve = new SimpleCurve
		{
			// life expectancy to price modifier
			new CurvePoint(0.15f,  0f), // 12 by human age, No whoring allowed for underage.
			new CurvePoint(0.22f,  1.5f), // 18
			new CurvePoint(0.3f, 1.4f), // 24
			new CurvePoint(0.4f, 1f), // 32
			new CurvePoint(0.6f, 0.75f), // 48
			new CurvePoint(1.0f, 0.5f), // 80
			new CurvePoint(5.0f, 0.5f), // Lifespan extended by bionics, misconfigurated alien races, etc.
		};

		public static int WhoreMinPrice(Pawn whore)
		{
			float min_price = 20f;
			min_price *= whoring_age_curve.Evaluate(SexUtility.ScaleToHumanAge(whore));

			if (xxx.has_traits(whore))
			{
				if (xxx.IndividualityIsActive && whore.story.traits.HasTrait(TraitDef.Named("SYR_Haggler")))
					min_price *= 1.5f;
				if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 2) // Industrious
					min_price *= 1.5f;
				if (whore.story.traits.DegreeOfTrait(TraitDefOf.NaturalMood) == -2) // Depressive
					min_price *= 0.5f;
				if (whore.story.traits.HasTrait(TraitDefOf.CreepyBreathing))
					min_price *= 0.75f;
				if (whore.story.traits.HasTrait(TraitDefOf.Beauty))
				{
					switch (whore.story.traits.DegreeOfTrait(TraitDefOf.Beauty))
					{
						case 2:
							min_price *= 3f;
							break;
						case 1:
							min_price *= 1.5f;
							break;
						case -1:
							min_price *= 0.5f;
							break;
						case -2:
							min_price *= 0.2f;
							break;
					}
				}
				if (whore.story.traits.HasTrait(xxx.masochist)) // Won't haggle, may settle for low price
					min_price *= 0.7f;
				if (whore.story.traits.HasTrait(xxx.nymphomaniac)) // Same as above
					min_price *= 0.4f;
			}

			min_price *= WhoreInjuryAdjustment(whore);
			if (whore.ownership.OwnedRoom != null)
				min_price *= WhoreRoomAdjustment(whore);
			return (int)min_price;
		}

		public static int WhoreMaxPrice(Pawn whore)
		{
			float max_price = 40f;
			max_price *= whoring_age_curve.Evaluate(SexUtility.ScaleToHumanAge(whore));

			if (whore.gender == Gender.Female)
				max_price *= 1.2f;
			if (xxx.has_traits(whore))
			{
				if (whore.story.traits.HasTrait(TraitDefOf.Greedy))
					max_price *= 2f;
				if (xxx.IndividualityIsActive && whore.story.traits.HasTrait(TraitDef.Named("SYR_Haggler")))
					max_price *= 1.5f;
				if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 2) // Industrious
					max_price *= 1.5f;
				if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 1) // Hard Worker
					max_price *= 1.2f;
				if (whore.story.traits.HasTrait(TraitDefOf.CreepyBreathing))
					max_price *= 0.75f;
				if (whore.story.traits.HasTrait(TraitDefOf.Beauty))
				{
					switch (whore.story.traits.DegreeOfTrait(TraitDefOf.Beauty))
					{
						case 2:
							max_price *= 3.5f;
							break;
						case 1:
							max_price *= 2f;
							break;
						case -1:
							max_price *= 0.8f;
							break;
						case -2:
							max_price *= 0.6f;
							break;
					}
				}
			}

			max_price *= WhoreInjuryAdjustment(whore);
			if (whore.ownership.OwnedRoom != null)
				max_price *= WhoreRoomAdjustment(whore);
			return (int)max_price;
		}

		public static float WhoreInjuryAdjustment(Pawn whore)
		{
			float modifier = 1.0f;
			int injuries = whore.health.hediffSet.hediffs.Count(x => x.Visible && x.def.everCurableByItem && x.IsPermanent());

			if (injuries == 0) return modifier;

			while (injuries > 0)
			{
				modifier *= 0.85f;
				injuries--;
			}
			return modifier;
		}

		public static float WhoreRoomAdjustment(Pawn whore)
		{
			float room_multiplier = 1f;
			Room ownedRoom = whore.ownership.OwnedRoom;

			if (ownedRoom == null) return 0f;

			//Room sharing is not liked by patrons
			room_multiplier = room_multiplier / (2 * (ownedRoom.Owners.Count() - 1) + 1);
			int scoreStageIndex = RoomStatDefOf.Impressiveness.GetScoreStageIndex(ownedRoom.GetStat(RoomStatDefOf.Impressiveness));
			//Room impressiveness factor
			//0 < scoreStageIndex < 10 (Last time checked)
			//3 is mediocore
			if (scoreStageIndex == 0) { room_multiplier *= 0.3f; }
			if (scoreStageIndex > 3) { room_multiplier *= 1 + (scoreStageIndex - 3) / 3; } //top room triples the price
			return room_multiplier;
		}

		public static int PriceOfWhore(Pawn whore)
		{
			float NeedSexFactor = xxx.need_some_sex(whore) > 1 ? 1 - xxx.need_some_sex(whore) / 8 : 1f;

			float price = Rand.Range(WhoreMinPrice(whore), WhoreMaxPrice(whore));

			price *= NeedSexFactor;
			//--Log.Message("[RJW] xxx::PriceOfWhore - price is " + price);

			return (int)Math.Round(price);
		}

		public static bool CanAfford(Pawn targetPawn, Pawn whore, int priceOfWhore = -1)
		{
			if (targetPawn.Faction == whore.Faction) return true;

			int price = priceOfWhore < 0 ? PriceOfWhore(whore) : priceOfWhore;
			if (price == 0)
				return true;

			Lord lord = targetPawn.GetLord();
			Faction faction = targetPawn.Faction;
			int totalAmountOfSilvers = targetPawn.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver);

			if (faction != null)
			{
				List<Pawn> caravanMembers = targetPawn.Map.mapPawns.PawnsInFaction(targetPawn.Faction).Where(x => x.GetLord() == lord && x.inventory?.innerContainer?.TotalStackCountOfDef(ThingDefOf.Silver) > 0).ToList();
				if (!caravanMembers.Any())
				{
					//--Log.Message("[RJW]CanAfford::(" + xxx.get_pawnname(targetPawn) + "," + xxx.get_pawnname(whore) + ") - totalAmountOfSilvers is " + totalAmountOfSilvers);
					return totalAmountOfSilvers >= price;
				}

				totalAmountOfSilvers += caravanMembers.Sum(animal => animal.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver));
			}

			//--Log.Message("[RJW]CanAfford:: caravan cannot afford the price");
			return totalAmountOfSilvers >= price;
		}

		//priceOfWhore is assumed >=0, and targetPawn is assumed to be able to pay the price(either by caravan, or by himself)
		//This means that targetPawn has total stackcount of silvers >= priceOfWhore.
		public static int PayPriceToWhore(Pawn targetPawn, int priceOfWhore, Pawn whore)
		{
			int AmountLeft = priceOfWhore;
			if (targetPawn.Faction == whore.Faction || priceOfWhore == 0)
			{
				//--Log.Message("[RJW] xxx::PayPriceToWhore - No need to pay price");
				return AmountLeft;
			}
			Lord lord = targetPawn.GetLord();
			//Caravan guestCaravan = Find.WorldObjects.Caravans.Where(x => x.Spawned && x.ContainsPawn(targetPawn) && x.Faction == targetPawn.Faction && !x.IsPlayerControlled).FirstOrDefault();
			List<Pawn> caravanAnimals = targetPawn.Map.mapPawns.PawnsInFaction(targetPawn.Faction).Where(x => x.GetLord() == lord && x.inventory?.innerContainer != null && x.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver) > 0).ToList();

			IEnumerable<Thing> TraderSilvers;
			if (!caravanAnimals.Any())
			{
				TraderSilvers = targetPawn.inventory.innerContainer.Where(x => x.def == ThingDefOf.Silver);
				foreach (Thing silver in TraderSilvers)
				{
					if (AmountLeft <= 0)
						return AmountLeft;
					int dropAmount = silver.stackCount >= AmountLeft ? AmountLeft : silver.stackCount;
					if (targetPawn.inventory.innerContainer.TryDrop(silver, whore.Position, whore.Map, ThingPlaceMode.Near, dropAmount, out Thing resultingSilvers))
					{
						if (resultingSilvers is null)
						{
							//--Log.Message("[RJW] xxx::PayPriceToWhore - silvers is null0");
							return AmountLeft;
						}
						AmountLeft -= resultingSilvers.stackCount;
						if (AmountLeft <= 0)
						{
							return AmountLeft;
						}
					}
					else
					{
						//--Log.Message("[RJW] xxx::PayPriceToWhore - TryDrop failed0");
						return AmountLeft;
					}
				}
				return AmountLeft;
			}

			foreach (Pawn animal in caravanAnimals)
			{
				TraderSilvers = animal.inventory.innerContainer.Where(x => x.def == ThingDefOf.Silver);
				foreach (Thing silver in TraderSilvers)
				{
					if (AmountLeft <= 0)
						return AmountLeft;
					int dropAmount = silver.stackCount >= AmountLeft ? AmountLeft : silver.stackCount;
					if (animal.inventory.innerContainer.TryDrop(silver, whore.Position, whore.Map, ThingPlaceMode.Near, dropAmount, out Thing resultingSilvers))
					{
						if (resultingSilvers is null)
						{
							//--Log.Message("[RJW] xxx::PayPriceToWhore - silvers is null1");
							return AmountLeft;
						}
						AmountLeft -= resultingSilvers.stackCount;
						if (AmountLeft <= 0)
						{
							return AmountLeft;
						}
					}
				}
			}
			return AmountLeft;
		}

		public static bool IsTargetPawnOkay(Pawn pawn)
		{
			return xxx.is_healthy(pawn) && !pawn.Downed && !pawn.Suspended;
		}

		public static bool IsHookupAppealing(Pawn target, Pawn whore)
		{
			if (PawnUtility.WillSoonHaveBasicNeed(target))
			{
				return false;
			}
			float num = target.relations.SecondaryRomanceChanceFactor(whore) / 1.5f;
			if (xxx.need_some_sex(target) > 2f)
			{
				num *= 3.0f;
			}
			else if (xxx.need_some_sex(target) > 1f)
			{
				num *= 2.0f;
			}
			if (xxx.is_zoophile(target) && !xxx.is_animal(whore))
			{
				num *= 0.5f;
			}
			if (xxx.AlienFrameworkIsActive)
			{
				if (xxx.is_xenophile(target))
				{
					if (target.def.defName == whore.def.defName)
						num *= 0.5f; // Same species, xenophile less interested.
					else
						num *= 1.5f; // Different species, xenophile more interested.
				}
				else if (xxx.is_xenophobe(target))
				{
					if (target.def.defName != whore.def.defName)
						num *= 0.25f; // Different species, xenophobe less interested.
				}
			}

			num *= 0.8f + ((float)whore.skills.GetSkill(SkillDefOf.Social).Level / 40); // 0.8 to 1.3
			num *= Mathf.InverseLerp(-100f, 0f, target.relations.OpinionOf(whore));
			return Rand.Range(0.05f, 1f) < num;
		}

		// Summary:
		//   Check if the pawn is willing to hook up. Checked for both target and the whore.
		public static bool WillPawnTryHookup(Pawn target)
		{
			if (xxx.RomanceDiversifiedIsActive && target.story.traits.HasTrait(xxx.asexual))
			{
				return false;
			}
			Pawn lover = LovePartnerRelationUtility.ExistingMostLikedLovePartner(target, false);
			if (lover == null)
			{
				return true;
			}
			float num = target.relations.OpinionOf(lover);
			float num2 = Mathf.InverseLerp(30f, -80f, num);

			if (xxx.is_prude(target))
			{
				num2 = 0f;
			}
			else if (xxx.is_lecher(target))
			{
				//Lechers are always up for it.
				num2 = Mathf.InverseLerp(100f, 50f, num);
			}
			else if (target.Map == lover.Map)
			{
				//Less likely to cheat if the lover is on the same map.
				num2 = Mathf.InverseLerp(70f, 15f, num);
			}
			//else default values

			if (xxx.need_some_sex(target) > 2)
			{
				num2 *= 1.8f;
			}
			else if (xxx.need_some_sex(target) > 1)
			{
				num2 *= 1.4f;
			}
			num2 /= 1.5f;
			return Rand.Range(0f, 1f) < num2;
		}
	}
}


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

namespace rjw
{
	public static class bondage_gear_tradeability
	{
		public static void init()
		{
			// Allows bondage gear to be selled by traders
			if (xxx.config.bondage_gear_enabled)
			{
				foreach (var def in DefDatabase<bondage_gear_def>.AllDefs)
					def.tradeability = Tradeability.Sellable;
			}
			// Forbids bondage gear to be selled by traders
			else
			{
				foreach (var def in DefDatabase<bondage_gear_def>.AllDefs)
					def.tradeability = Tradeability.None;
			}
		}
	}

	public static class bondage_gear_extensions
	{
		public static bool has_lock(this Apparel app)
		{
			return (app.TryGetComp<CompHoloCryptoStamped>() != null);
		}

		public static bool is_wearing_locked_apparel(this Pawn p)
		{
			if (p.apparel != null)
				foreach (var app in p.apparel.WornApparel)
					if (app.has_lock())
						return true;
			return false;
		}

		// Tries to get p started on the job of using an item on either another pawn or on themself (if "other" is null).
		// Of course in order for this method to work, the item's useJob has to be able to handle use on another pawn. This
		// is true for the holokey and bondage gear in RJW but not the items in the core game
		public static void start_job(this CompUsable usa, Pawn p, LocalTargetInfo tar)
		{
			if (p.CanReserveAndReach(usa.parent, PathEndMode.Touch, Danger.Some) &&
				((tar == null) || p.CanReserveAndReach(tar, PathEndMode.Touch, Danger.Some)))
			{
				var comfor = usa.parent.GetComp<CompForbiddable>();
				if (comfor != null)
					comfor.Forbidden = false;
				var job = new Job(((CompProperties_Usable)usa.props).useJob, usa.parent, tar);
				p.jobs.TryTakeOrderedJob(job);
			}
		}

		// Creates a menu option to use an item. "tar" is expected to be a pawn, corpse or null if it doesn't apply (in which
		// case the pawn will presumably use the item on themself). "required_work" can also be null.
		public static FloatMenuOption make_option(this CompUsable usa, string label, Pawn p, LocalTargetInfo tar, WorkTypeDef required_work)
		{
			if ((tar != null) && (!p.CanReserve(tar)))
			{
				string key = "Reserved";
				string text = TranslatorFormattedStringExtensions.Translate(key);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else if ((tar != null) && (!p.CanReach(tar, PathEndMode.Touch, Danger.Some)))
			{
				string key = "NoPath";
				string text = TranslatorFormattedStringExtensions.Translate(key);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else if ((required_work != null) && p.story.WorkTypeIsDisabled(required_work))
			{
				string key = "CannotPrioritizeWorkTypeDisabled";
				string text = TranslatorFormattedStringExtensions.Translate(key, required_work.gerundLabel);
				return new FloatMenuOption(label + " (" + text + ")", null, MenuOptionPriority.DisabledOption);
			}
			else
				return new FloatMenuOption(
					label,
					delegate
					{
						usa.start_job(p, tar);
					},
					MenuOptionPriority.Default);
		}
	}

	public class bondage_gear_def : ThingDef
	{
		public Type soul_type;
		public HediffDef equipped_hediff = null;
		public bool gives_bound_moodlet = false;
		public bool gives_gagged_moodlet = false;
		public bool blocks_genitals = false;
		public bool blocks_anus = false;
		public bool blocks_breasts = false;
		private bondage_gear_soul soul_ins = null;

		public List<BodyPartDef> HediffTargetBodyPartDefs;      //field for optional list of targeted parts for hediff applying
		public List<BodyPartGroupDef> BoundBodyPartGroupDefs;   //field for optional list of groups restrained of verbcasting

		public bondage_gear_soul soul
		{
			get
			{
				if (soul_ins == null)
					soul_ins = (bondage_gear_soul)Activator.CreateInstance(soul_type);
				return soul_ins;
			}
		}
	}

	public class bondage_gear_soul
	{
		// Adds the bondage gear's associated HediffDef and spawns a matching holokey
		public virtual void on_wear(Pawn wearer, Apparel gear)
		{
			var def = (bondage_gear_def)gear.def;

			if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs != null)
			{
				foreach (BodyPartDef partDef in def.HediffTargetBodyPartDefs)                           //getting BodyPartDef, for example "Arm"
				{
					foreach (BodyPartRecord partRec in wearer.RaceProps.body.GetPartsWithDef(partDef))  //applying hediff to every single arm found on pawn
					{
						wearer.health.AddHediff(def.equipped_hediff, partRec);
					}
				}
			}
			else if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs == null)               //backward compatibility/simplified gear define without HediffTargetBodyPartDefs
			{                                                                                //Hediff applyed to whole body
				wearer.health.AddHediff(def.equipped_hediff);
			}

			var gear_stamp = gear.TryGetComp<CompHoloCryptoStamped>();
			if (gear_stamp != null)
			{
				var key = ThingMaker.MakeThing(ThingDef.Named("Holokey"));
				var key_stamp = key.TryGetComp<CompHoloCryptoStamped>();
				key_stamp.copy_stamp_from(gear_stamp);
				if (wearer.Map != null)
					GenSpawn.Spawn(key, wearer.Position, wearer.Map);
				else
					wearer.inventory.TryAddItemNotForSale(key);
			}
		}

		// Removes the gear's HediffDef
		public virtual void on_remove(Apparel gear, Pawn former_wearer)
		{
			var def = (bondage_gear_def)gear.def;
			if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs != null)
			{                                                                                //getting all Hediffs according with equipped_hediff def
				List<Hediff> hediffs = former_wearer.health.hediffSet.hediffs.Where(x => x.def == def.equipped_hediff).ToList();
				foreach (Hediff hedToRemove in hediffs)
				{
					if (def.HediffTargetBodyPartDefs.Contains(hedToRemove.Part.def))            //removing if applyed by this bondage_gear
						former_wearer.health.RemoveHediff(hedToRemove);                         //assuming there can be several different bondages
				}                                                                               //with the same equipped_hediff def
			}
			else if (def.equipped_hediff != null && def.HediffTargetBodyPartDefs == null)       //backward compatibility/simplified gear define without HediffTargetBodyPartDefs
			{
				var hed = former_wearer.health.hediffSet.GetFirstHediffOfDef(def.equipped_hediff);
				if (hed != null)
					former_wearer.health.RemoveHediff(hed);
			}
		}
	}

	// Give bondage gear an extremely low score when it's not being worn so pawns never equip it on themselves and give
	// it an extremely high score when it is being worn so pawns never try to take it off to equip something "better".
	public class bondage_gear : Apparel
	{
		public override float GetSpecialApparelScoreOffset()
		{
			return (Wearer == null) ? -1e5f : 1e5f;
		}

		// made this method universal for any bondage_gear, won't affect anything if gear's BoundBodyPartGroupDefs is empty or null
		public override bool AllowVerbCast(IntVec3 root, Map map, LocalTargetInfo targ, Verb verb)
		{
			if ((this.def as bondage_gear_def).BoundBodyPartGroupDefs != null &&
				verb.tool != null &&
				(this.def as bondage_gear_def).BoundBodyPartGroupDefs.Contains(verb.tool.linkedBodyPartsGroup))
			{
				return false;
			}
			return true;
		}

		//needed for save compatibility only
		public override void ExposeData()
		{
			base.ExposeData();
			//if (Scribe.mode == LoadSaveMode.PostLoadInit)
			//	CheckHediffs();
		}

		//save compatibility insurance, will prevent Armbinder hediff with 0part efficiency on whole body
		private void CheckHediffs()
		{
			var def = (bondage_gear_def)this.def;
			if (this.Wearer == null || def.equipped_hediff == null) return;

			bool changedHediff = false;
			void ApplyHediffDirect(HediffDef hedDef, BodyPartRecord partRec)
			{
				Hediff hediff = (Hediff)HediffMaker.MakeHediff(hedDef, Wearer);
				if (partRec != null)
					hediff.Part = partRec;
				Wearer.health.hediffSet.AddDirect(hediff);
				changedHediff = true;
			}

			void RemoveHediffDirect(Hediff hed)
			{
				Wearer.health.hediffSet.hediffs.Remove(hed);
				changedHediff = true;
			}

			List<bondage_gear> wornBondageGear = Wearer.apparel.WornApparel.Where(x => x is bondage_gear).Cast<bondage_gear>().ToList();
			List<Hediff> hediffs = new List<Hediff>();
			foreach (Hediff h in this.Wearer.health.hediffSet.hediffs) hediffs.Add(h);

			//checking current hediffs defined by bondage_gear for being on defined place, cleaning up the misplaced
			bool equippedHediff;
			bool onPlace;
			foreach (Hediff hed in hediffs)
			{
				equippedHediff = false;
				onPlace = false;
				foreach (bondage_gear gear in wornBondageGear)
				{
					if (hed.def == (gear.def as bondage_gear_def).equipped_hediff)
					{
						//if hediff from bondage_gear and on it's defined place then don't touch it else remove
						//assuming there can be several different bondages with the same equipped_hediff def and different hediff target parts, don't know why
						equippedHediff = true;

						if ((hed.Part != null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null))
						{
							//pass
						}
						else if ((hed.Part == null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null))
						{
							onPlace = true;
							break;
						}
						else if (hed.Part != null && (gear.def as bondage_gear_def).HediffTargetBodyPartDefs.Contains(hed.Part.def))
						{
							onPlace = true;
							break;
						}
					}
				}
				if (equippedHediff && !onPlace)
				{
					Log.Message("Removing Hediff " + hed.Label + " from " + Wearer + (hed.Part == null ? "'s body" : "'s " + hed.Part));
					RemoveHediffDirect(hed);
				}
			}

			// now iterating every gear for having all hediffs in place, adding missing
			foreach (bondage_gear gear in wornBondageGear)
			{
				if ((gear.def as bondage_gear_def).equipped_hediff == null) continue;
				if ((gear.def as bondage_gear_def).HediffTargetBodyPartDefs == null)                        //handling gear without HediffTargetBodyPartDefs
				{

					Hediff hed = Wearer.health.hediffSet.hediffs.Find(x => (x.def == (gear.def as bondage_gear_def).equipped_hediff && x.Part == null));//checking hediff defined by gear on whole body
					if (hed == null)                                                                        //if no legit hediff, adding
					{
						Log.Message("Adding missing Hediff " + (gear.def as bondage_gear_def).equipped_hediff.label + " to " + Wearer + "'s body");
						ApplyHediffDirect((gear.def as bondage_gear_def).equipped_hediff, null);
					}
				}
				else                                                                                //handling gear with defined HediffTargetBodyPartDefs
				{
					foreach (BodyPartDef partDef in (gear.def as bondage_gear_def).HediffTargetBodyPartDefs)    //getting every partDef
					{
						foreach (BodyPartRecord partRec in Wearer.RaceProps.body.GetPartsWithDef(partDef))      //checking all parts of def for applyed hediff
						{
							Hediff hed = Wearer.health.hediffSet.hediffs.Find(x => (x.def == (gear.def as bondage_gear_def).equipped_hediff && x.Part == partRec));//checking hediff defined by gear on defined place
							if (hed == null)                                                                    //if hediff missing, adding
							{
								Log.Message("Adding missing Hediff " + (gear.def as bondage_gear_def).equipped_hediff.label + " to " + Wearer + "'s " + partRec.Label);
								ApplyHediffDirect((gear.def as bondage_gear_def).equipped_hediff, partRec);
							}
						}
					}
				}
				//possibility of several different items with THE SAME equipped_hediff def ON THE SAME PART is not considered, that's sick
			}
			if (changedHediff)
			{
				//Possible error in current toil on Notify_HediffChanged() if capacity.Manipulation is involved so making it in the end.
				//Probably harmless, shouldn't happen again after corrected hediffs will be saved
				Wearer.health.Notify_HediffChanged(null);
			}
		}
	}

	public class armbinder : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class yoke : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class Restraints : bondage_gear
	{
		// Prevents pawns in armbinders from melee attacking
		//public override bool AllowVerbCast (IntVec3 root, TargetInfo targ)
		//{
		//	return false;
		//}
	}

	public class force_off_gear_def : RecipeDef
	{
		public ThingDef removes_apparel;
		public BodyPartDef failure_affects;
		public List<BodyPartDef> destroys_one_of = null;
		public List<BodyPartDef> major_burns_on = null;
		public List<BodyPartDef> minor_burns_on = null;
	}
}

repo_name: chippy/rjw

license: mit


code:
using Verse;

namespace rjw
{
	public class config : Def
	{
		// TODO: Clean these.
		// Commented out old configs that are no longer in use (or have been converted into settings).

		// Feature Toggles
		//public bool comfort_prisoners_enabled;
		//public bool colonists_can_be_comfort_prisoners;
		//public bool cum_enabled;
		//public bool rape_me_sticky_enabled;
		//public bool sounds_enabled;
		//public bool stds_enabled;
		public bool bondage_gear_enabled;
		//public bool nymph_joiners_enabled;
		//public bool whore_beds_enabled;
		//public bool necro_enabled;
		//public bool random_rape_enabled;
		//public bool always_accept_whores;
		//public bool nymphs_always_JoinInBed;
		//public bool zoophis_always_rape;
		//public bool rapists_always_rape;
		//public bool pawns_always_do_fapping;
		//public bool pawns_always_rapeCP;
		//public bool whores_always_findjob;

		// Display Toggles
		public bool show_regular_dick_and_vag;

		// STD config
		public bool std_show_roll_to_catch;

		public float std_min_severity_to_pitch;
		public float std_env_pitch_cleanliness_exaggeration;
		public float std_env_pitch_dirtiness_exaggeration;
		public float std_outdoor_cleanliness;

		// Age Config
		//public int sex_free_for_all_age;
		//public int sex_minimum_age;

		public float minor_pain_threshold;	        // 0.3
		public float significant_pain_threshold;	// 0.6
		public float extreme_pain_threshold;		// 0.95
		public float base_chance_to_hit_prisoner;	// 50
		public int min_ticks_between_hits;			// 500
		public int max_ticks_between_hits;			// 700

		public float max_nymph_fraction;
		public float opp_inf_initial_immunity;
		public float comfort_prisoner_rape_mtbh_mul;
		//public float whore_mtbh_mul;
		public float nymph_spawn_with_std_mul;
		//public float chance_to_rim;

		//public float fertility_endAge_male;
		//public float fertility_endAge_female_humanlike;
		//public float fertility_endAge_female_animal;
	}
}


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

namespace RimWorld
{
	public class racedefs : Def
	{
		public float fertility_endAge_male = 0.2f;
		public float fertility_endAge_female = 0.1f;
	}
}


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

namespace rjw
{
	public class std_def : Def
	{
		public HediffDef hediff_def;
		public HediffDef cohediff_def = null;
		public float catch_chance;
		public float environment_pitch_chance = 0.0f;
		public float spawn_chance = 0.0f;
		public float spawn_severity = 0.0f;
		public float autocure_below_severity = -1.0f;
		public bool applied_on_genitals;
	}

	public static class std
	{
		public static std_def hiv;
		public static std_def herpes;
		public static std_def warts;
		public static std_def syphilis;
		public static readonly HediffDef immunodeficiency = DefDatabase<HediffDef>.GetNamed("Immunodeficiency");

		public static void init()
		{
			hiv = DefDatabase<std_def>.GetNamed("HIV");
			herpes = DefDatabase<std_def>.GetNamed("Herpes");
			warts = DefDatabase<std_def>.GetNamed("Warts");
			syphilis = DefDatabase<std_def>.GetNamed("Syphilis");
		}

		public static List<std_def> all => DefDatabase<std_def>.AllDefsListForReading;

		// Returns how severely affected this pawn's crotch is by rashes and warts, on a scale from 0 to 3.
		public static int genital_rash_severity(Pawn p)
		{
			int tr = 0;

			Hediff her = p.health.hediffSet.GetFirstHediffOfDef(herpes.hediff_def);
			if (her != null && her.Severity >= 0.25f)
				++tr;

			Hediff war = p.health.hediffSet.GetFirstHediffOfDef(warts.hediff_def);
			if (war != null)
				tr += war.Severity < 0.40f ? 1 : 2;

			return tr;
		}

		public static Hediff get_infection(Pawn p, std_def sd)
		{
			return p.health.hediffSet.GetFirstHediffOfDef(sd.hediff_def);
		}

		public static float get_severity(Pawn p, std_def sd)
		{
			Hediff hed = get_infection(p, sd);
			return hed?.Severity ?? 0.0f;
		}

		public static Hediff infect(Pawn p, std_def sd, bool include_coinfection = true)
		{
			Hediff existing = get_infection(p, sd);
			if (existing == null)
			{
				BodyPartRecord part = sd.applied_on_genitals ? xxx.genitals : null;
				p.health.AddHediff(sd.hediff_def, part);
				if (include_coinfection && sd.cohediff_def != null)
					p.health.AddHediff(sd.cohediff_def, part);
				//--Log.Message("[RJW]std::infect genitals std");
				return get_infection(p, sd);
			}
			else
				return existing;
		}

		public static void show_infection_letter(Pawn p, std_def sd, string source = null, float? chance = null)
		{
			StringBuilder info;
			{
				info = new StringBuilder();
				info.Append(xxx.get_pawnname(p) + " has caught " + sd.label + (source != null ? " from " + source + "." : ""));
				if (chance.HasValue)
					info.Append(" (" + chance.Value.ToStringPercent() + " chance)");
				info.AppendLine(); info.AppendLine();
				info.Append(sd.description);
			}
			Find.LetterStack.ReceiveLetter("Infection: " + sd.label, info.ToString(), LetterDefOf.ThreatSmall, p);
		}

		public static void update(Pawn p)
		{
			update_immunodeficiency(p);

			// Check if any infections are below the autocure threshold and cure them if so
			foreach (std_def sd in all)
			{
				Hediff inf = get_infection(p, sd);
				if (inf != null && (inf.Severity < sd.autocure_below_severity || !RJWSettings.stds_enabled ||
					p.health.hediffSet.HasHediff(Genital_Helper.archotech_penis) || p.health.hediffSet.HasHediff(Genital_Helper.archotech_vagina)))
				{
					p.health.RemoveHediff(inf);
					if (sd.cohediff_def != null)
					{
						Hediff coinf = p.health.hediffSet.GetFirstHediffOfDef(sd.cohediff_def);
						if (coinf != null)
							p.health.RemoveHediff(coinf);
					}
				}
			}

			roll_for_syphilis_damage(p);
		}

		public static bool is_wasting_away(Pawn p)
		{
			Hediff id = p.health.hediffSet.GetFirstHediffOfDef(immunodeficiency);
			return id != null && id.CurStageIndex > 0;
		}

		public static void update_immunodeficiency(Pawn p)
		{
			float min_bf_for_id = 1.0f - immunodeficiency.minSeverity;
			Hediff id = p.health.hediffSet.GetFirstHediffOfDef(immunodeficiency);
			float bf = p.health.capacities.GetLevel(PawnCapacityDefOf.BloodFiltration);
			bool has = id != null;
			bool should_have = bf <= min_bf_for_id;

			if (has && !should_have)
			{
				p.health.RemoveHediff(id);
				id = null;
			}
			else if (!has && should_have)
			{
				p.health.AddHediff(immunodeficiency);
				id = p.health.hediffSet.GetFirstHediffOfDef(immunodeficiency);
			}

			if (id == null) return;

			id.Severity = 1.0f - bf;

			// Roll for and apply opportunistic infections:
			// Pawns will have a 90% chance for at least one infection each year at 0% filtration, and a 0%
			// chance at 40% filtration, scaling linearly.
			// Let x = chance infected per roll
			// Then chance not infected per roll = 1 - x
			// And chance not infected on any roll in one day = (1 - x) ^ (60000 / 150) = (1 - x) ^ 400
			// And chance not infected on any roll in one year = (1 - x) ^ (400 * 60) = (1 - x) ^ 24000
			// So 0.10 = (1 - x) ^ 24000
			//	log (0.10) = 24000 log (1 - x)
			//	x = 0.00009593644334648975435114691213 = ~96 in 1 million
			// Important Note:
			// this function is called from Need_Sex::NeedInterval(), where it involves a needsex_tick and a std_tick to actually trigger this update_immunodeficiency.
			// j(this is not exactly the same as the value in Need_Sex, that value is 0, but here j should be 1) std_ticks per this function called, k needsex_ticks per std_tick, 150 ticks per needsex_tick, and x is the chance per 150 ticks,
			// The new equation should be .1 = (1-x)^(24000/kj)
			// log(.1) = (24000/kj) log(1-x),  so log(1-x)= (kj/24000) log(.1), 1-x = .1^(kj/24000), x= 1-.1^(kj/24000)
			// Since k=10,j=1, so kj=10, new x is 1-.1^(10/24000)=0.0009589504, let it be 959/1000000
			if (Rand.RangeInclusive(1, 1000000) <= 959 && Rand.Value < bf / min_bf_for_id)
			{
				BodyPartRecord part;
				{
					float rv = Rand.Value;
					if (rv < 0.25f)
						part = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Jaw"));
					else if (rv < 0.50f)
						part = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Lung"));
					else if (rv < 0.75f)
						part = BodyDefOf.Human.AllParts.FindLast(bpr => string.Equals(bpr.def.defName, "Lung"));
					else
						part = BodyDefOf.Human.AllParts.RandomElement();
				}

				if (part != null &&
					!p.health.hediffSet.PartIsMissing(part) && !p.health.hediffSet.HasDirectlyAddedPartFor(part) &&
					p.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.WoundInfection) == null && // If the pawn already has a wound infection, we can't properly set the immunity for the new one
					p.health.immunity.GetImmunity(HediffDefOf.WoundInfection) <= 0.0f)
				{ // Dont spawn infection if pawn already has immunity
					p.health.AddHediff(HediffDefOf.WoundInfection, part);
					p.health.HealthTick(); // Creates the immunity record
					ImmunityRecord ir = p.health.immunity.GetImmunityRecord(HediffDefOf.WoundInfection);
					if (ir != null)
						ir.immunity = xxx.config.opp_inf_initial_immunity;
					const string message_title = "Opportunistic Infection";
					string message_text = "RJW_Opportunistic_Infection_Message".Translate(xxx.get_pawnname(p));
					Find.LetterStack.ReceiveLetter(message_title, message_text, LetterDefOf.ThreatSmall);
				}
			}
		}

		public static void roll_to_catch(Pawn catcher, Pawn pitcher)
		{
			if (!RJWSettings.stds_enabled) return;

			// Archotech genitalia automagically purge any known STDs.
			if (catcher.health.hediffSet.HasHediff(Genital_Helper.archotech_vagina) || pitcher.health.hediffSet.HasHediff(Genital_Helper.archotech_vagina) ||
				catcher.health.hediffSet.HasHediff(Genital_Helper.archotech_penis) || pitcher.health.hediffSet.HasHediff(Genital_Helper.archotech_penis)) return;

			bool has_artificial_genitals = catcher.health.hediffSet.HasDirectlyAddedPartFor(xxx.genitals);
			float thru_genitals = !has_artificial_genitals ? 1.00f : 0.15f;
			float on_genitals = !has_artificial_genitals ? 1.00f : 0.00f;

			// Roll for and apply food poisoning from rimming(a.k.a. licking anus)
			// Edited by nizhuan-jjr: get food poisoning by licking anus makes no senses, so I choose to commnet out and save some computation here.
			/*
			if (Genital_Helper.has_anus(catcher)&&(Rand.Value < xxx.config.chance_to_rim) && (!pitcher.health.hediffSet.HasHediff(HediffDefOf.FoodPoisoning))) {
				pitcher.health.AddHediff(HediffDefOf.FoodPoisoning);
				if (PawnUtility.ShouldSendNotificationAbout(pitcher)) {
					Messages.Message("RJW_Poisoned_by_Rimming_Message".Translate(new object[] {xxx.get_pawnname(pitcher), xxx.get_pawnname(catcher)}), pitcher, MessageTypeDefOf.NegativeEvent);
				}
			}
			*/

			float cleanliness_factor;
			{
				Room room = catcher.GetRoom();
				float cle = room?.GetStat(RoomStatDefOf.Cleanliness) ?? xxx.config.std_outdoor_cleanliness;
				float exa = cle >= 0.0f ? xxx.config.std_env_pitch_cleanliness_exaggeration : xxx.config.std_env_pitch_dirtiness_exaggeration;
				cleanliness_factor = Mathf.Max(0.0f, 1.0f - exa * cle);
			}

			if (xxx.config.std_show_roll_to_catch)
				//--Log.Message(xxx.get_pawnname(catcher) + " is rolling to catch STDs (cleanliness factor: " + cleanliness_factor.ToString() + ")" + ((!has_artificial_genitals) ? "" : " (has artificial genitals)"));

				foreach (std_def sd in all)
				{
					if (!catcher.health.hediffSet.HasHediff(sd.hediff_def))
					{
						if (catcher.health.immunity.GetImmunity(sd.hediff_def) <= 0.0f)
						{
							float catch_chance = sd.catch_chance * (sd.applied_on_genitals ? on_genitals : thru_genitals);
							float catch_rv = Rand.Value;
							if (xxx.config.std_show_roll_to_catch)
								//--Log.Message("  Chance to catch " + sd.label + ": " + catch_chance.ToStringPercent() + "; rolled: " + catch_rv.ToString());
								if (catch_rv < catch_chance)
								{
									string pitch_source; float pitch_chance;
									{
										if (get_severity(pitcher, sd) >= xxx.config.std_min_severity_to_pitch)
										{
											pitch_source = xxx.get_pawnname(pitcher);
											pitch_chance = 1.0f;
										}
										else
										{
											pitch_source = "the environment";
											pitch_chance = sd.environment_pitch_chance * cleanliness_factor;
											if (!RJWSettings.std_floor)
											{
												pitch_chance = -9001f;
											}
										}
									}
									float pitch_rv = Rand.Value;

									if (xxx.config.std_show_roll_to_catch)
										//--Log.Message("	Chance to pitch (from " + pitch_source + "): " + pitch_chance.ToStringPercent() + "; rolled: " + pitch_rv.ToString());
										if (pitch_rv < pitch_chance)
										{
											infect(catcher, sd);
											show_infection_letter(catcher, sd, pitch_source, catch_chance * pitch_chance);
											//if (xxx.config.std_show_roll_to_catch)
											//--Log.Message("	  INFECTED!");
										}
								}
						}
						//else
						//if (xxx.config.std_show_roll_to_catch)
						//--Log.Message("  Still immune to " + sd.label);
					}
					//else
					//if (xxx.config.std_show_roll_to_catch)
					//--Log.Message("  Already infected with " + sd.label);
				}
		}

		public static void generate_on(Pawn p)
		{
			if (p == null) return;
			//prevent error on world gen for pawns with broken bodies(no genitals)
			if (p.RaceProps.body.HasPartWithTag(BodyPartTagDefOf.RJW_FertilitySource))
				return;
			if (!xxx.is_human(p))
				return;
			float nymph_mul = !xxx.is_nympho(p) ? 1.0f : xxx.config.nymph_spawn_with_std_mul;
			foreach (std_def sd in all)
				if (Rand.Value < sd.spawn_chance * nymph_mul)
				{
					Hediff hed = infect(p, sd, false);
					float sev;
					{
						float r = Rand.Range(sd.hediff_def.minSeverity, sd.hediff_def.maxSeverity);
						sev = Mathf.Clamp(sd.spawn_severity * r, sd.hediff_def.minSeverity, sd.hediff_def.maxSeverity);
					}
					hed.Severity = sev;
				}
		}

		public static void roll_for_syphilis_damage(Pawn p)
		{
			Hediff syp = p.health.hediffSet.GetFirstHediffOfDef(syphilis.hediff_def);
			if (syp == null || !(syp.Severity >= 0.60f) || syp.FullyImmune()) return;

			// A 30% chance per day of getting any permanent damage works out to ~891 in 1 million for each roll
			// The equation is (1-x)^(60000/150)=.7
			// Important Note:
			// this function is called from Need_Sex::NeedInterval(), where it involves a needsex_tick and a std_tick to actually trigger this roll_for_syphilis_damage.
			// j(this is not exactly the same as the value in Need_Sex, that value is 0, but here j should be 1) std_ticks per this function called, k needsex_ticks per std_tick, 150 ticks per needsex_tick, and x is the chance per 150 ticks,
			// The new equation should be .7 = (1-x)^(400/kj)
			// 1-x = .7^(kj/400), x =1-.7^(kj/400)
			// Since k=10,j=1, so kj=10, new x is 1-.7^(10/400)=0.0088772362, let it be 888/100000
			if (Rand.RangeInclusive(1, 100000) <= 888)
			{
				BodyPartRecord part;
				float sev;

				float rv = Rand.Value;
				if (rv < 0.10f)
				{
					part = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Brain"));
					sev = 1.0f;
				}
				else if (rv < 0.50f)
				{
					part = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Liver"));
					sev = Rand.RangeInclusive(1, 3);
				}
				else if (rv < 0.75f)
				{
					//LeftKidney, probably
					part = BodyDefOf.Human.AllParts.Find(bpr => string.Equals(bpr.def.defName, "Kidney"));
					sev = Rand.RangeInclusive(1, 2);
				}
				else
				{
					//RightKidney, probably
					part = BodyDefOf.Human.AllParts.FindLast(bpr => string.Equals(bpr.def.defName, "Kidney"));
					sev = Rand.RangeInclusive(1, 2);
				}

				if (part != null && !p.health.hediffSet.PartIsMissing(part) && !p.health.hediffSet.HasDirectlyAddedPartFor(part))
				{
					DamageDef vir_dam = DefDatabase<DamageDef>.GetNamed("ViralDamage");
					HediffDef dam_def = HealthUtility.GetHediffDefFromDamage(vir_dam, p, part);
					Hediff_Injury inj = (Hediff_Injury)HediffMaker.MakeHediff(dam_def, p, null);
					inj.Severity = sev;
					inj.TryGetComp<HediffComp_GetsPermanent>().IsPermanent = true;
					p.health.AddHediff(inj, part, null);
					string message_title = syphilis.label + " Damage";
					string baby_pronoun = p.gender == Gender.Male ? "his" : "her";
					string message_text = "RJW_Syphilis_Damage_Message".Translate(xxx.get_pawnname(p), baby_pronoun, part.def.label, syphilis.label);
					Find.LetterStack.ReceiveLetter(message_title, message_text, LetterDefOf.ThreatSmall, p);
				}
			}
		}
	}
}