using System.Collections.Generic; using System.Linq; using UnityEngine; using HarmonyLib; using RimWorld; using Verse; using Verse.AI; using rjw; using Rimworld_Animations; using RJW_ToysAndMasturbation; namespace Rimworld_Animations_Patch { [HarmonyPatch(typeof(JobDriver_Sex), "setup_ticks")] public static class HarmonyPatch_JobDriver_Sex_setup_ticks { public static void Postfix(ref JobDriver_Sex __instance) { // Sets ticks so that the orgasm meter starts empty, plus stop any running animations HarmonyPatch_JobDriver_Masturbate_setup_ticks.Postfix(ref __instance); // Invite another for a threesome? if (RJWHookupSettings.QuickHookupsEnabled && __instance is JobDriver_SexBaseInitiator && __instance.pawn.GetAllSexParticipants().Count == 2 && (__instance is JobDriver_JoinInSex) == false && Random.value < BasicSettings.chanceForOtherToJoinInSex) { DebugMode.Message("Find another to join in sex"); Pawn pawn = __instance.pawn; List candidates = new List(); float radius = 4f; foreach (Thing thing in GenRadial.RadialDistinctThingsAround(pawn.Position, pawn.Map, radius, true)) { Pawn other = thing as Pawn; // Find candidates to invite if (other != null && (int)SexInteractionUtility.CheckSexJobAgainstMorals(other, __instance, out Precept precept) <= 0 && SexInteractionUtility.PawnCanInvitePasserbyForSex(other, pawn.GetAllSexParticipants())) { DebugMode.Message(other.NameShortColored + " is a potential candidate"); candidates.Add(other); } } // Invite a random candidate (weighted by attraction) if (candidates.Count > 0) { Pawn invitedPawn = candidates.RandomElementByWeight(x => SexAppraiser.would_fuck(pawn, x, false, false, true) + SexAppraiser.would_fuck(pawn.GetSexPartner(), x, false, false, true)); pawn.GetSexInitiator().IsInBed(out Building bed); DebugMode.Message(invitedPawn.NameShortColored + " was invited to join in sex"); Job job = new Job(DefDatabase.GetNamed("JoinInSex", false), pawn.GetSexPartner(), bed); invitedPawn.jobs.TryTakeOrderedJob(job); } } } } [HarmonyPatch(typeof(JobDriver_Masturbate), "setup_ticks")] public static class HarmonyPatch_JobDriver_Masturbate_setup_ticks { // Sets ticks so that the orgasm meter starts empty, plus stop any running animations public static void Postfix(ref JobDriver_Sex __instance) { __instance.sex_ticks = __instance.duration; CompBodyAnimator comp = __instance.pawn.TryGetComp(); if (comp != null && comp.isAnimating) { comp.isAnimating = false; } } } [HarmonyPatch(typeof(JobDriver_SexBaseInitiator), "Start")] public static class HarmonyPatch_JobDriver_SexBaseInitiator_Start { public static bool MustRerollHumping(Pawn pawn, SexProps sexProps) { if (sexProps?.dictionaryKey?.defName == null || sexProps.dictionaryKey.defName != "Masturbation_Humping") { return false; } if (pawn.IsInBed(out Building bed)) { return false; } DebugMode.Message("Not in bed, cannot do requested action"); return true; } public static float RandomMasturbationWeights(InteractionDef interactionDef, Pawn pawn) { bool hasBed = pawn.IsInBed(out Building bed); if (interactionDef.defName == "Masturbation_Breastjob" && Genital_Helper.has_breasts(pawn)) { return BasicSettings.breastsMasturbationChance; } if (interactionDef.defName == "Masturbation_HandjobA" && Genital_Helper.has_anus(pawn)) { return BasicSettings.analMasturbationChance; } if (interactionDef.defName == "Masturbation_HandjobP" && Genital_Helper.has_penis_fertile(pawn)) { return BasicSettings.genitalMasturbationChance; } if (interactionDef.defName == "Masturbation_HandjobV" && Genital_Helper.has_vagina(pawn)) { return BasicSettings.genitalMasturbationChance; } if (interactionDef.defName == "Masturbation_Humping" && hasBed) { return BasicSettings.humpingMasturbationChance; } return 0f; } // Adds weights to masturbation type selection public static void Prefix(ref JobDriver_SexBaseInitiator __instance) { if (__instance.Sexprops == null) { __instance.Sexprops = __instance.pawn.GetRMBSexPropsCache(); } if (__instance is JobDriver_Masturbate && (__instance.Sexprops == null || MustRerollHumping(__instance.pawn, __instance.Sexprops))) { DebugMode.Message("No valid sexprops provided. Generating new interaction..."); SexProps sexProps = new SexProps(); sexProps.pawn = __instance.pawn; sexProps.partner = __instance.pawn; sexProps.sexType = xxx.rjwSextype.Masturbation; List interactionDefs = DefDatabase.AllDefs.Where(x => x.HasModExtension()).ToList(); Dictionary interactionsPlusWeights = new Dictionary(); foreach (InteractionDef interactionDef in interactionDefs) { var interaction = rjw.Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(interactionDef); if (interaction.Extension.rjwSextype != xxx.rjwSextype.Masturbation.ToStringSafe()) { continue; } interactionsPlusWeights.Add(interaction, RandomMasturbationWeights(interaction.Interaction, sexProps.pawn)); } var selectedInteraction = interactionsPlusWeights.RandomElementByWeight(x => x.Value).Key; sexProps.dictionaryKey = selectedInteraction.Interaction; sexProps.rulePack = selectedInteraction.Extension.rulepack_defs.RandomElement(); DebugMode.Message("Generated interaction: " + sexProps.dictionaryKey.defName); DebugMode.Message(sexProps.rulePack); __instance.Sexprops = sexProps; } } // Adds in option for animated masturbation public static void Postfix(ref JobDriver_SexBaseInitiator __instance) { // Allow solo animations to be played if (__instance is JobDriver_Masturbate && __instance.pawn.GetAnimationData() == null) { PickMasturbationAnimation(__instance.pawn, __instance.Sexprops); } // Allow make out animations to be played if (__instance.pawn.GetAnimationData() == null) { PickMakeOutAnimation(__instance.pawn, __instance.Sexprops); } // If there is no animation to play, exit if (__instance.pawn.GetAnimationData() == null) { return; } // Get animation data AnimationDef anim = __instance.pawn.GetAnimationData()?.animationDef; List pawnsToAnimate = __instance.pawn.GetAllSexParticipants(); // Sync animations across participants foreach (Pawn participant in pawnsToAnimate) { JobDriver_Sex jobdriver = participant.jobs.curDriver as JobDriver_Sex; if (jobdriver == null) { continue; } // Animation timing reset jobdriver.orgasms = 0; jobdriver.ticks_left = AnimationPatchUtility.FindTrueAnimationLength(participant, out int orgasmTick); jobdriver.ticksLeftThisToil = jobdriver.ticks_left; jobdriver.sex_ticks = orgasmTick; jobdriver.duration = jobdriver.sex_ticks; jobdriver.orgasmstick = 0; // Reset anchor and animation for sex toys CompThingAnimator sexToyCompThingAnimator = ((Thing)jobdriver.job.GetTarget(TargetIndex.A)).TryGetComp(); if (sexToyCompThingAnimator != null) { DebugMode.Message("Using sex toy - " + jobdriver.job.GetTarget(TargetIndex.A)); __instance.pawn.IsInBed(out Building bed); Vector3 anchor = AnimationPatchUtility.GetAnchorPosition(__instance.pawn, bed) - new Vector3(0.5f, 0, 0.5f); AccessTools.Field(typeof(CompThingAnimator), "anchor").SetValue(sexToyCompThingAnimator, anchor); } // Determine where pawns are to toss clothes if (participant?.apparel?.WornApparel != null) { IntVec3 apparelCell = MathUtility.FindRandomCellNearPawn(participant, 4); foreach (Apparel apparel in participant.apparel.WornApparel) { CompApparelVisibility compApparelVisibility = apparel.TryGetComp(); if (compApparelVisibility != null) { compApparelVisibility.GenerateFloorPosition(apparelCell, new Vector2(0f, 0.125f)); } } } } } public static void PickMasturbationAnimation(Pawn pawn, SexProps sexProps = null) { if (pawn.TryGetComp() == null) { Log.Error("Error: " + pawn.Name + " of race " + pawn.def.defName + " does not have CompBodyAnimator attached!"); return; } pawn.TryGetComp().isAnimating = false; List pawnsToAnimate = new List() { pawn }; AnimationDef anim = null; // Get random animation based on interaction type if (sexProps != null) { var interaction = rjw.Modules.Interactions.Helpers.InteractionHelper.GetWithExtension(sexProps.dictionaryKey); InteractionDef interactionDef = interaction.Interaction; DebugMode.Message("Finding animations that match " + interactionDef.defName); List anims = new List(); foreach (AnimationDef _anim in DefDatabase.AllDefs) { if (_anim?.actors?.Count == 1 && _anim.sexTypes != null && _anim.sexTypes.Contains(xxx.rjwSextype.Masturbation) && _anim.interactionDefTypes != null && _anim.interactionDefTypes.Contains(interactionDef.defName) && AnimationUtility.GenitalCheckForPawn(_anim.actors[0].requiredGenitals, pawn, out string failReason)) { anims.Add(_anim); } } if (anims != null && anims.Any()) { anim = anims.RandomElement(); } } // If no animation exists, pick one at random if (anim == null) { anim = AnimationUtility.tryFindAnimation(ref pawnsToAnimate, xxx.rjwSextype.Masturbation, sexProps); } if (anim == null) { DebugMode.Message("No animation found"); return; } // Start animation DebugMode.Message("Playing " + anim.defName); pawn.IsInBed(out Building bed); if (bed != null) { pawn.TryGetComp().setAnchor(bed); } else { pawn.TryGetComp().setAnchor(pawn.Position); } pawn.TryGetComp().StartAnimation(anim, pawnsToAnimate, 0, GenTicks.TicksGame % 2 == 0, true, bed == null); // Hide hearts if necessary if (!AnimationSettings.hearts) { (pawn.jobs.curDriver as JobDriver_Sex).ticks_between_hearts = System.Int32.MaxValue; } } public static void PickMakeOutAnimation(Pawn pawn, SexProps sexProps = null) { if (pawn.TryGetComp() == null) { Log.Error("Error: " + pawn.Name + " of race " + pawn.def.defName + " does not have CompBodyAnimator attached!"); return; } List pawnsToAnimate = pawn.GetAllSexParticipants(); if (sexProps.sexType != xxx.rjwSextype.Oral || pawnsToAnimate.Count != 2) { return; } List kissingAnims = DefDatabase.AllDefs.Where(x => x.defName.Contains("Kiss")).ToList(); AnimationDef anim = kissingAnims[Random.Range(0, kissingAnims.Count)]; if (anim == null) { DebugMode.Message("No animation found"); return; } bool mirror = GenTicks.TicksGame % 2 == 0; // Start animation DebugMode.Message("Playing " + anim.defName); foreach (Pawn participant in pawnsToAnimate) { participant.TryGetComp().setAnchor(pawnsToAnimate[0].Position); participant.TryGetComp().StartAnimation(anim, pawnsToAnimate, pawnsToAnimate.IndexOf(participant), mirror); // Hide hearts if necessary if (!AnimationSettings.hearts) { (participant.jobs.curDriver as JobDriver_Sex).ticks_between_hearts = System.Int32.MaxValue; } } } } [HarmonyPatch(typeof(JobDriver_Sex), "SexTick")] public static class HarmonyPatch_JobDriver_Sex_SexTick { // If pawns don't have privacy, they'll stop having sex public static void Postfix(ref JobDriver_Sex __instance, Pawn pawn) { if (pawn.IsHashIntervalTick(90)) { if (pawn.IsMasturbating() && pawn.HasPrivacy(8f) == false) { pawn.jobs.EndCurrentJob(JobCondition.InterruptForced, false, false); } else if (pawn.IsHavingSex()) { bool havePrivacy = true; List participants = pawn.GetAllSexParticipants(); foreach (Pawn participant in participants) { if (participant.HasPrivacy(8f) == false) { havePrivacy = false; } } if (__instance.Sexprops != null && (__instance.Sexprops.isRape || __instance.Sexprops.isWhoring)) { return; } if (havePrivacy == false) { foreach (Pawn participant in participants) { participant.jobs.EndCurrentJob(JobCondition.InterruptForced, false, false); } } } } } } [HarmonyPatch(typeof(JobDriver_Sex), "Orgasm")] public static class HarmonyPatch_JobDriver_Sex_Orgasm { // Stops orgasm triggering more than once per animation public static bool Prefix(ref JobDriver_Sex __instance) { if (__instance.orgasms > 0) { return false; } return true; } public static bool ParticipantsDesireMoreSex(JobDriver_Sex jobdriver) { List participants = jobdriver.pawn.GetAllSexParticipants(); float satisfaction = 0f; foreach (Pawn pawn in participants) { Need_Sex sexNeed = pawn?.needs?.TryGetNeed(); if (sexNeed == null) { satisfaction += 1; continue; } satisfaction += sexNeed.CurLevelPercentage; } return Rand.Chance(1 - satisfaction / participants.Count); } // Alows the starting of a new animation cycle at the end of the current one public static void Postfix(ref JobDriver_Sex __instance) { if (__instance.orgasms > 0) { __instance.sex_ticks = 0; } if (__instance is JobDriver_SexBaseInitiator == false || __instance is JobDriver_JoinInSex) { return; } if (__instance.Sexprops != null && (__instance.Sexprops.isRape || __instance.Sexprops.isWhoring)) { return; } if (__instance.ticksLeftThisToil <= 1 && (__instance.neverendingsex || ParticipantsDesireMoreSex(__instance))) { List participants = __instance.pawn.GetAllSexParticipants(); if (participants.Count == 2) { Job job = JobMaker.MakeJob(participants[0].CurJobDef, participants[0], participants[0].jobs.curJob.targetC); participants[1].jobs.StartJob(job, JobCondition.Succeeded); } } } } [HarmonyPatch(typeof(JobDriver_SexBaseInitiator), "End")] public static class HarmonyPatch_JobDriver_Sex_End { // Clear all partners out when sex ends to prevent issues with threesome animations public static void Postfix(ref JobDriver_SexBaseInitiator __instance) { if (__instance.Partner != null && __instance?.Partner?.jobs?.curDriver != null && __instance.Partner.Dead == false && __instance.Partner?.jobs.curDriver is JobDriver_SexBaseReciever) { foreach (Pawn participant in (__instance.Partner?.jobs.curDriver as JobDriver_SexBaseReciever).parteners.ToList()) { participant.jobs.EndCurrentJob(JobCondition.Succeeded, false, true); } (__instance.Partner?.jobs.curDriver as JobDriver_SexBaseReciever).parteners.Clear(); } } } [HarmonyPatch(typeof(SexUtility), "AfterMasturbation")] public static class HarmonyPatch_SexUtility_AfterMasturbation { // Removes excess calls to generate filth public static bool Prefix(SexProps props) { var methodInfo = AccessTools.Method(typeof(SexUtility), "IncreaseTicksToNextLovin", null, null); methodInfo.Invoke(null, new object[] { props.pawn }); AfterSexUtility.UpdateRecords(props); return false; } } [HarmonyPatch(typeof(Genital_Helper), "has_mouth")] public static class HarmonyPatch_Genital_Helper_has_mouth { // Fixes mouth check public static bool Prefix(ref bool __result, Pawn pawn) { __result = pawn.health.hediffSet.GetNotMissingParts().Any(x => x.def.defName.ToLower().ContainsAny("mouth", "teeth", "jaw", "beak")); return false; } } [HarmonyPatch(typeof(SexUtility), "DrawNude")] public static class HarmonyPatch_SexUtility_DrawNude { public static bool Prefix(Pawn pawn, bool keep_hat_on) { if (!xxx.is_human(pawn)) return false; if (pawn.Map != Find.CurrentMap) return false; pawn.Drawer.renderer.graphics.ClearCache(); pawn.Drawer.renderer.graphics.apparelGraphics.Clear(); ApparelAnimationUtility.DetermineApparelToKeepOn(pawn); foreach (Apparel apparel in pawn.apparel.WornApparel) { CompApparelVisibility comp = apparel.TryGetComp(); if ((comp == null || comp.isBeingWorn) && ApparelGraphicRecordGetter.TryGetGraphicApparel(apparel, pawn.story.bodyType, out ApparelGraphicRecord item)) { pawn.Drawer.renderer.graphics.apparelGraphics.Add(item); } } GlobalTextureAtlasManager.TryMarkPawnFrameSetDirty(pawn); return false; } } }