diff --git a/1.3/Source/Mod/Whoring.csproj b/1.3/Source/Mod/Whoring.csproj index 3030a5d..80f1f7d 100644 --- a/1.3/Source/Mod/Whoring.csproj +++ b/1.3/Source/Mod/Whoring.csproj @@ -40,7 +40,7 @@ False - ..\..\..\..\..\..\..\workshop\content\294100\818773962\v1.2\Assemblies\HugsLib.dll + ..\..\..\..\..\..\..\workshop\content\294100\818773962\v1.3\Assemblies\HugsLib.dll False diff --git a/1.4/Assemblies/RimJobWorldWhoring.dll b/1.4/Assemblies/RimJobWorldWhoring.dll new file mode 100644 index 0000000..af89fa2 Binary files /dev/null and b/1.4/Assemblies/RimJobWorldWhoring.dll differ diff --git a/1.4/Defs/JobDefs/Jobs_Whoring.xml b/1.4/Defs/JobDefs/Jobs_Whoring.xml new file mode 100644 index 0000000..d3e6bed --- /dev/null +++ b/1.4/Defs/JobDefs/Jobs_Whoring.xml @@ -0,0 +1,17 @@ + + + + + WhoreIsServingVisitors + rjwwhoring.JobDriver_WhoreIsServingVisitors + serving visitors + false + + + + WhoreInvitingVisitors + rjwwhoring.JobDriver_WhoreInvitingVisitors + attempting hookup + false + + \ No newline at end of file diff --git a/1.4/Defs/RecordDefs/Records.xml b/1.4/Defs/RecordDefs/Records.xml new file mode 100644 index 0000000..f94897f --- /dev/null +++ b/1.4/Defs/RecordDefs/Records.xml @@ -0,0 +1,18 @@ + + + + + + CountOfWhore + + The number of times I whored myself. + Int + + + + EarnedMoneyByWhore + + The amount of silvers I have earned as a whore. + Int + + diff --git a/1.4/Defs/Rooms/RoomRoles.xml b/1.4/Defs/Rooms/RoomRoles.xml new file mode 100644 index 0000000..855f0bd --- /dev/null +++ b/1.4/Defs/Rooms/RoomRoles.xml @@ -0,0 +1,17 @@ + + + + + Brothel + + rjwwhoring.RoomRoleWorker_Brothel + +
  • Beauty
  • +
  • Cleanliness
  • +
  • Wealth
  • +
  • Space
  • +
  • Impressiveness
  • +
    +
    + +
    \ No newline at end of file diff --git a/1.4/Defs/ThinkTreeDefs/ThinkTrees_Prisoner.xml b/1.4/Defs/ThinkTreeDefs/ThinkTrees_Prisoner.xml new file mode 100644 index 0000000..8af3edf --- /dev/null +++ b/1.4/Defs/ThinkTreeDefs/ThinkTrees_Prisoner.xml @@ -0,0 +1,22 @@ + + + + + PrisonerWhoreSexualEmergencyTree + Humanlike_PostDuty + 100 + + +
  • + +
  • + +
  • + +
  • +
    + + +
    +
    +
    \ No newline at end of file diff --git a/1.4/Defs/ThinkTreeDefs/ThinkTrees_Whore.xml b/1.4/Defs/ThinkTreeDefs/ThinkTrees_Whore.xml new file mode 100644 index 0000000..def7e6b --- /dev/null +++ b/1.4/Defs/ThinkTreeDefs/ThinkTrees_Whore.xml @@ -0,0 +1,43 @@ + + + + + + WhoreSexualEmergencyTree + Humanlike_PreMain + 15 + + +
  • + +
  • + +
  • + +
  • +
    + + +
    +
    + + + + WhoreJobTree + Humanlike_PostMain + 15 + + +
  • + +
  • + +
  • + +
  • +
    + + +
    +
    +
    \ No newline at end of file diff --git a/1.4/Defs/ThoughtDefs/Thoughts_Whore.xml b/1.4/Defs/ThoughtDefs/Thoughts_Whore.xml new file mode 100644 index 0000000..0bbd196 --- /dev/null +++ b/1.4/Defs/ThoughtDefs/Thoughts_Whore.xml @@ -0,0 +1,125 @@ + + + + + Whorish_Thoughts + rjwwhoring.ThoughtWorker_Whore + 2.0 + 10 + 0.4 + + + +
  • 30
  • +
  • 50
  • +
    + 30 + +
  • + + We just need money! I'm not a ... + -5 +
  • +
  • + + Well, at least it pays well... + -1 +
  • +
  • + + This job isn't so bad afterall! + 2 +
  • +
    +
    + + + Whorish_Thoughts_Captive + rjwwhoring.ThoughtWorker_Whore + 4.0 + 10 + 0.4 + + + +
  • 30
  • +
  • 40
  • +
  • 80
  • +
    + 10 + + +
  • + + They forced me to serve as a sex toy! + -20 +
  • +
  • + + I'm not just a rental ride! + -10 +
  • +
  • + + Just no beatings, please. + -1 +
  • +
  • + + I could get more clients if not these restraints! + 2 +
  • +
    +
    + + + + RJWFailedSolicitation + Thought_MemorySocial + 0.4 + 100 + 1 + 0.5 + +
  • + + -1 +
  • +
    +
    + + + + RJWTurnedDownWhore + Thought_MemorySocial + 0.2 + 1 + 1 + 0.5 + +
  • + + -1 +
  • +
    +
    + + + SleptInBrothel + 1 + 1 + 1 + +
  • + + Eww, the sheets were all sticky. + -10 +
  • +
  • + + I just love this place, the smell, the sounds... + 2 +
  • +
    +
    +
    diff --git a/1.4/Defs/TipSetDefs/Tips.xml b/1.4/Defs/TipSetDefs/Tips.xml new file mode 100644 index 0000000..4f36f27 --- /dev/null +++ b/1.4/Defs/TipSetDefs/Tips.xml @@ -0,0 +1,13 @@ + + + + RjwWhoringTips + + +
  • Whoring price is visible in the "show sexuality" menu on the bio tab (the icon looks like a heart).
  • + + +
  • Condoms can be automatically used if placed in a stockpile next to a bed - it might be a good idea to have some around if you intend on whoring your colonists out.
  • +
    +
    +
    \ No newline at end of file diff --git a/1.4/Defs/WhoreBackstories.xml b/1.4/Defs/WhoreBackstories.xml new file mode 100644 index 0000000..329d45c --- /dev/null +++ b/1.4/Defs/WhoreBackstories.xml @@ -0,0 +1,30 @@ + + + + + + WhoreBackstories + + + + + +
  • Sex slave
  • +
  • Courtesean
  • +
  • Housemate
  • +
  • Model
  • +
  • idol
  • + +
  • Nymph
  • + +
  • Pleasure
  • +
  • Courtesan
  • + +
  • Dancer
  • +
  • Holo-star
  • + +
  • Prostitute
  • +
  • Breeder
  • +
    +
    +
    \ No newline at end of file diff --git a/1.4/Defs/WhoringTab/PawnColumnDefs/PawnColumns.xml b/1.4/Defs/WhoringTab/PawnColumnDefs/PawnColumns.xml new file mode 100644 index 0000000..63985a5 --- /dev/null +++ b/1.4/Defs/WhoringTab/PawnColumnDefs/PawnColumns.xml @@ -0,0 +1,52 @@ + + + + RJW_IsWhore + Whores + rjwwhoring.MainTab.PawnColumnWorker_IsWhore + true + 80 + + + RJW_WhoreExperience + Whoring experience + + rjwwhoring.MainTab.PawnColumnWorker_WhoreExperience + 100 + + + RJW_PriceRangeOfWhore + Price range for whore + + rjwwhoring.MainTab.PawnColumnWorker_PriceRangeOfWhore + 100 + + + RJW_EarnedMoneyByWhore + Money earned(total) + + rjwwhoring.MainTab.PawnColumnWorker_EarnedMoneyByWhore + 100 + + + RJW_CountOfWhore + Clients served + + rjwwhoring.MainTab.PawnColumnWorker_CountOfWhore + 100 + + + RJW_AverageMoneyByWhore + Money earned(average) + + rjwwhoring.MainTab.PawnColumnWorker_AverageMoneyByWhore + 100 + + + RJW_WhoreMood + Mood of pawn + + rjwwhoring.MainTab.PawnColumnWorker_Mood + 100 + + diff --git a/1.4/Defs/WhoringTab/PawnTableDefs.xml b/1.4/Defs/WhoringTab/PawnTableDefs.xml new file mode 100644 index 0000000..f321b97 --- /dev/null +++ b/1.4/Defs/WhoringTab/PawnTableDefs.xml @@ -0,0 +1,33 @@ + + + + + RJW_Brothel + rjwwhoring.MainTab.PawnTable_Whores + +
  • Label
  • +
  • GapTiny
  • +
  • RJW_Gender
  • +
  • GapTiny
  • +
  • LifeStage
  • +
  • GapTiny
  • +
  • RJW_IsPrisoner
  • +
  • RJW_IsSlave
  • +
  • GapTiny
  • +
  • RJW_IsWhore
  • +
  • RJW_WhoreMood
  • +
  • RJW_CountOfWhore
  • +
  • RJW_WhoreExperience
  • +
  • RJW_EarnedMoneyByWhore
  • +
  • RJW_AverageMoneyByWhore
  • +
  • RJW_PriceRangeOfWhore
  • +
  • RemainingSpace
  • +
    + +
  • + +
  • +
    +
    + +
    diff --git a/1.4/Languages/English/Keyed/Whoring.xml b/1.4/Languages/English/Keyed/Whoring.xml new file mode 100644 index 0000000..401855b --- /dev/null +++ b/1.4/Languages/English/Keyed/Whoring.xml @@ -0,0 +1,43 @@ + + + Enable whoring tab + Shows/hide whoring tab + Show whore price factor on beds + Show whore price factor as a label on beds that are enabled for whoring. + Show bed widgets + Show bed widgets to mark beds for public/private for whoring. + + Money Printing + Clients will spawn silver instead of using their own/caravan + Client RNG disable + Instead of doing rng roll, Clients always accept solicitation. + + Debug log whoring info + Enables some very spamming debug logs to help find bugs in Whoring solicitation operations. + + + {0} accepted the deal {1} offered. + {0} wants to be serviced by {1}. + {0} rejected the deal {1} offered. + {0} rejected the offer because {1} does not look healthy + + + Assign to whorin' + + Won't agree to be a whore + + + Allow everyone to whore + Allow all whores to use this bed to entertain customers. + Allow owner to whore + Whether owner(s) are allowed to use this bed for whoring. + Whoring price factor (based on comfort, room impressiveness and number of beds in room: {0} + + Whoring price range: + Prisoner + Slave + \ No newline at end of file diff --git a/1.4/Patches/FacialAnimation_compatibility.xml b/1.4/Patches/FacialAnimation_compatibility.xml new file mode 100644 index 0000000..8cae3d9 --- /dev/null +++ b/1.4/Patches/FacialAnimation_compatibility.xml @@ -0,0 +1,27 @@ + + + + +
  • [NL] Facial Animation - WIP
  • +
    + + Always + +
  • + /Defs/FacialAnimation.FaceAnimationDef[defName="Lovin" or defName="Lovin2"]/targetJobs + Always + +
  • WhoreIsServingVisitors
  • + + +
  • + /Defs/FacialAnimation.FaceAnimationDef[defName="StandAndBeSociallyActive"]/targetJobs + Always + +
  • WhoreInvitingVisitors
  • + + +
    +
    +
    +
    \ No newline at end of file diff --git a/1.4/Source/Mod/Data/BedData.cs b/1.4/Source/Mod/Data/BedData.cs new file mode 100644 index 0000000..5d6a1ff --- /dev/null +++ b/1.4/Source/Mod/Data/BedData.cs @@ -0,0 +1,39 @@ +using System; +using Verse; +using System.Linq; +using RimWorld; + +namespace rjwwhoring +{ + public class BedData : IExposable + { + public Building_Bed bed = null; + public bool allowedForWhoringOwner = true; + public bool allowedForWhoringAll = false; + public int reservedUntilGameTick = 0; + public int reservedForPawnID = 0; + + public int lastScoreUpdateTick = -70; // GenTicks.TicksGame + public float bedScore = -1f; + public int scoreUpdateTickDelay = 60; + + public float roomScore = -1f; + + public BedData() { } + public BedData(Building_Bed bed) + { + this.bed = bed; + } + + public void ExposeData() + { + Scribe_References.Look(ref bed, "Bed"); + Scribe_Values.Look(ref allowedForWhoringOwner, "allowedForWhoringOwner", true, true); + Scribe_Values.Look(ref allowedForWhoringAll, "allowedForWhoringAll", false, true); + Scribe_Values.Look(ref reservedUntilGameTick, "lastUsed", 0, true); + Scribe_Values.Look(ref reservedForPawnID, "lastUsedBy", 0, true); + } + + public bool IsValid { get { return bed != null; } } + } +} diff --git a/1.4/Source/Mod/Data/DataStore.cs b/1.4/Source/Mod/Data/DataStore.cs new file mode 100644 index 0000000..1f3566b --- /dev/null +++ b/1.4/Source/Mod/Data/DataStore.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Verse; +using RimWorld; +using RimWorld.Planet; + +namespace rjwwhoring +{ + /// + /// Rimworld object for storing the world/save info + /// + public class DataStore : WorldComponent + { + public Dictionary bedData = new Dictionary(); + + public DataStore(World world) : base(world) + { + } + + public override void ExposeData() + { + if (Scribe.mode == LoadSaveMode.Saving) + { + bedData.RemoveAll(item => item.Value == null || !item.Value.IsValid); + } + + base.ExposeData(); + Scribe_Collections.Look(ref bedData, "BedData", LookMode.Value, LookMode.Deep); + if (Scribe.mode == LoadSaveMode.LoadingVars) + { + if (bedData == null) bedData = new Dictionary(); + } + } + + public BedData GetBedData(Building_Bed bed) + { + BedData res; + var filled = bedData.TryGetValue(bed.thingIDNumber, out res); + if ((res == null) || (!res.IsValid)) + { + if (filled) + { + bedData.Remove(bed.thingIDNumber); + } + res = new BedData(bed); + bedData.Add(bed.thingIDNumber, res); + } + return res; + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/Data/StringListDef.cs b/1.4/Source/Mod/Data/StringListDef.cs new file mode 100644 index 0000000..541b00c --- /dev/null +++ b/1.4/Source/Mod/Data/StringListDef.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Verse; + +namespace rjwwhoring +{ + /// + /// Whore backstories from xml GET! + /// Just a simplest form of passing data from xml to the code + /// + public class StringListDef : Def + { + public List strings = new List(); + } +} diff --git a/1.4/Source/Mod/DefOf/RecordDefDefOf.cs b/1.4/Source/Mod/DefOf/RecordDefDefOf.cs new file mode 100644 index 0000000..0a13f1f --- /dev/null +++ b/1.4/Source/Mod/DefOf/RecordDefDefOf.cs @@ -0,0 +1,12 @@ +using RimWorld; +using Verse; + +namespace rjwwhoring +{ + [DefOf] + public static class RecordDefOf + { + public static RecordDef EarnedMoneyByWhore; + public static RecordDef CountOfWhore; + } +} diff --git a/1.4/Source/Mod/DefOf/ThoughtDefOf.cs b/1.4/Source/Mod/DefOf/ThoughtDefOf.cs new file mode 100644 index 0000000..b7a2e14 --- /dev/null +++ b/1.4/Source/Mod/DefOf/ThoughtDefOf.cs @@ -0,0 +1,19 @@ +using RimWorld; +using Verse; + +namespace rjwwhoring +{ + [DefOf] + public static class ThoughtDefOf + { + public static ThoughtDef_Whore Whorish_Thoughts; + + public static ThoughtDef_Whore Whorish_Thoughts_Captive; + + public static ThoughtDef SleptInBrothel; + + public static ThoughtDef RJWFailedSolicitation; + + public static ThoughtDef RJWTurnedDownWhore; + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/JobDrivers/JobDriver_WhoreInvitingVisitors.cs b/1.4/Source/Mod/JobDrivers/JobDriver_WhoreInvitingVisitors.cs new file mode 100644 index 0000000..3e6aa9a --- /dev/null +++ b/1.4/Source/Mod/JobDrivers/JobDriver_WhoreInvitingVisitors.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using RimWorld; +using rjw; +using Verse; +using Verse.AI; + +namespace rjwwhoring +{ + public class JobDriver_WhoreInvitingVisitors : JobDriver + { + // List of jobs that can be interrupted by whores. + public static readonly List allowedJobs = new List { null, JobDefOf.Wait_Wander, JobDefOf.GotoWander, JobDefOf.Clean, JobDefOf.ClearSnow, + JobDefOf.CutPlant, JobDefOf.HaulToCell, JobDefOf.Deconstruct, JobDefOf.Harvest, JobDefOf.LayDown, JobDefOf.Research, JobDefOf.SmoothFloor, JobDefOf.SmoothWall, + JobDefOf.SocialRelax, JobDefOf.StandAndBeSociallyActive, JobDefOf.RemoveApparel, JobDefOf.Strip, JobDefOf.Tame, JobDefOf.Wait, JobDefOf.Wear, JobDefOf.FixBrokenDownBuilding, + JobDefOf.FillFermentingBarrel, JobDefOf.DoBill, JobDefOf.Sow, JobDefOf.Shear, JobDefOf.BuildRoof, JobDefOf.DeliverFood, JobDefOf.HaulToContainer, JobDefOf.Hunt, JobDefOf.Mine, + JobDefOf.OperateDeepDrill, JobDefOf.OperateScanner, JobDefOf.RearmTurret, JobDefOf.Refuel, JobDefOf.RefuelAtomic, JobDefOf.RemoveFloor, JobDefOf.RemoveRoof, JobDefOf.Repair, + JobDefOf.TakeBeerOutOfFermentingBarrel, JobDefOf.Train, JobDefOf.Uninstall, xxx.Masturbate}; + + public bool successfulPass = true; + + private Pawn Whore => GetActor(); + private Pawn TargetPawn => TargetThingA as Pawn; + private Building_Bed TargetBed => TargetThingB as Building_Bed; + + private readonly TargetIndex TargetPawnIndex = TargetIndex.A; + private readonly TargetIndex TargetBedIndex = TargetIndex.B; + + private bool DoesTargetPawnAcceptAdvance() + { + if (WhoringBase.DebugWhoring) ModLog.Message($"JobDriver_InvitingVisitors::DoesTargetPawnAcceptAdvance() - {xxx.get_pawnname(TargetPawn)}"); + //if (RJWSettings.WildMode) return true; + + if (PawnUtility.EnemiesAreNearby(TargetPawn)) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" fail - enemy near"); + return false; + } + if (!allowedJobs.Contains(TargetPawn.jobs.curJob.def)) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" fail - not allowed job"); + return false; + } + + if (WhoringBase.DebugWhoring) + { + ModLog.Message("Will try hookup " + WhoringHelper.WillPawnTryHookup(TargetPawn)); + ModLog.Message("Is whore appealing " + WhoringHelper.IsHookupAppealing(TargetPawn, Whore)); + ModLog.Message("Can afford whore " + WhoringHelper.CanAfford(TargetPawn, Whore)); + ModLog.Message("Need sex " + (xxx.need_some_sex(TargetPawn) >= 1)); + } + if (WhoringHelper.WillPawnTryHookup(TargetPawn) && WhoringHelper.IsHookupAppealing(TargetPawn, Whore) && WhoringHelper.CanAfford(TargetPawn, Whore) && xxx.need_some_sex(TargetPawn) >= 1f) + { + if (!Whore.IsPrisoner) + Whore.skills.Learn(SkillDefOf.Social, 1.2f); + return true; + } + return false; + } + + public override bool TryMakePreToilReservations(bool errorOnFailed) => true; + + protected override IEnumerable MakeNewToils() + { + this.FailOnDespawnedOrNull(TargetPawnIndex); + this.FailOnDespawnedNullOrForbidden(TargetBedIndex); + this.FailOn(() => Whore is null || !WhoreBed_Utility.CanUseForWhoring(Whore, TargetBed));//|| !Whore.CanReserve(TargetPawn) + this.FailOn(() => pawn.Drafted); + + if (!Whore.IsPrisoner) + { + yield return Toils_Goto.GotoThing(TargetPawnIndex, PathEndMode.Touch); + + + Toil TryItOn = new Toil(); + TryItOn.AddFailCondition(() => !xxx.IsTargetPawnOkay(TargetPawn)); + TryItOn.defaultCompleteMode = ToilCompleteMode.Delay; + TryItOn.initAction = delegate + { + //ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - TryItOn - initAction is called"); + Whore.jobs.curDriver.ticksLeftThisToil = 50; + FleckMaker.ThrowMetaIcon(Whore.Position, Whore.Map, FleckDefOf.Heart); + }; + yield return TryItOn; + } + + Toil AwaitResponse = new Toil(); + AwaitResponse.defaultCompleteMode = ToilCompleteMode.Delay; + AwaitResponse.defaultDuration = 10; + AwaitResponse.initAction = delegate + { + List extraSentencePacks = new List(); + successfulPass = DoesTargetPawnAcceptAdvance(); + //ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - AwaitResponse - initAction is called"); + if (successfulPass) + { + FleckMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, FleckDefOf.Heart); + TargetPawn.jobs.EndCurrentJob(JobCondition.Incompletable); + if (xxx.RomanceDiversifiedIsActive) + { + extraSentencePacks.Add(RulePackDef.Named("HookupSucceeded")); + } + if (Whore.health.HasHediffsNeedingTend()) + { + successfulPass = false; + string key = "RJW_VisitorSickWhore"; + string text = key.Translate(TargetPawn.LabelIndefinite(), Whore.LabelIndefinite()).CapitalizeFirst(); + Messages.Message(text, Whore, MessageTypeDefOf.TaskCompletion); + } + else + { + string key = "RJW_VisitorAcceptWhore"; + if (Whore.IsPrisoner) + { + key = "RJW_VisitorSolicitWhore"; + } + string text = key.Translate(TargetPawn.LabelIndefinite(), Whore.LabelIndefinite()).CapitalizeFirst(); + Messages.Message(text, TargetPawn, MessageTypeDefOf.TaskCompletion); + } + } + if (!successfulPass) + { + // remove bed reservation + TargetBed.UnreserveForWhoring(); + + FleckMaker.ThrowMetaIcon(TargetPawn.Position, TargetPawn.Map, FleckDefOf.IncapIcon); + TargetPawn.needs.mood.thoughts.memories.TryGainMemory( + TargetPawn.Faction == Whore.Faction + ? ThoughtDef.Named("RJWTurnedDownWhore") + : ThoughtDef.Named("RJWFailedSolicitation"), Whore); + + if (xxx.RomanceDiversifiedIsActive) + { + Whore.needs.mood.thoughts.memories.TryGainMemory(ThoughtDef.Named("RebuffedMyHookupAttempt"), TargetPawn); + TargetPawn.needs.mood.thoughts.memories.TryGainMemory(ThoughtDef.Named("FailedHookupAttemptOnMe"), Whore); + extraSentencePacks.Add(RulePackDef.Named("HookupFailed")); + } + //Disabled rejection notifications + //Messages.Message("RJW_VisitorRejectWhore".Translate(new object[] { xxx.get_pawnname(TargetPawn), xxx.get_pawnname(Whore) }), TargetPawn, MessageTypeDefOf.SilentInput); + } + if (xxx.RomanceDiversifiedIsActive) + { + Find.PlayLog.Add(new PlayLogEntry_Interaction(DefDatabase.GetNamed("TriedHookupWith"), pawn, TargetPawn, extraSentencePacks)); + } + //Log.Message("[RJW] AwaitResponse initAction - successfulPass: " + successfulPass.ToString()); + }; + yield return AwaitResponse; + + Toil BothGoToBed = new Toil(); + BothGoToBed.AddFailCondition(() => !successfulPass || !WhoreBed_Utility.CanUseForWhoring(Whore, TargetBed)); + BothGoToBed.defaultCompleteMode = ToilCompleteMode.Instant; + BothGoToBed.initAction = delegate + { + //ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - initAction is called0"); + if (!successfulPass) return; + if (!WhoreBed_Utility.CanUseForWhoring(Whore, TargetBed) && Whore.CanReserve(TargetPawn, 1, 0)) + { + //ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - cannot use the whore bed"); + return; + } + //ModLog.Message("JobDriver_InvitingVisitors::MakeNewToils - BothGoToBed - initAction is called1"); + + TargetBed.ReserveForWhoring(Whore, 1800);//is 1800 ticks long enough to go to the bed (until next reservation is made?) + + Whore.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(xxx.whore_is_serving_visitors, TargetPawn, TargetBed)); + //TargetPawn.jobs.jobQueue.EnqueueFirst(JobMaker.MakeJob(DefDatabase.GetNamed("WhoreIsServingVisitors"), Whore, TargetBed, (TargetBed.MaxAssignedPawnsCount>1)?TargetBed.GetSleepingSlotPos(1): TargetBed.)), null); + Whore.jobs.curDriver.EndJobWith(JobCondition.InterruptOptional); + //TargetPawn.jobs.curDriver.EndJobWith(JobCondition.InterruptOptional); + }; + yield return BothGoToBed; + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/JobDrivers/JobDriver_WhoreIsServingVisitors.cs b/1.4/Source/Mod/JobDrivers/JobDriver_WhoreIsServingVisitors.cs new file mode 100644 index 0000000..ecd45d2 --- /dev/null +++ b/1.4/Source/Mod/JobDrivers/JobDriver_WhoreIsServingVisitors.cs @@ -0,0 +1,183 @@ +using System.Collections.Generic; +using RimWorld; +using Verse; +using Verse.AI; +//using Multiplayer.API; +using rjw; + +namespace rjwwhoring +{ + public class JobDriver_WhoreIsServingVisitors : JobDriver_SexBaseInitiator + { + public IntVec3 SleepSpot => Bed.SleepPosOfAssignedPawn(pawn); + + public override bool TryMakePreToilReservations(bool errorOnFailed) + { + return pawn.Reserve(Target, job, 1, 0, null, errorOnFailed); + } + + //[SyncMethod] + protected override IEnumerable MakeNewToils() + { + if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":MakeNewToils() - making toils"); + setup_ticks(); + var PartnerJob = xxx.gettin_loved; + + this.FailOnDespawnedOrNull(iTarget); + this.FailOnDespawnedNullOrForbidden(iBed); + + if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":fail conditions check " + !WhoreBed_Utility.CanUseForWhoring(pawn, Bed) + " " + !pawn.CanReserve(Partner)); + this.FailOn(() => !WhoreBed_Utility.CanUseForWhoring(pawn, Bed) || !pawn.CanReserve(Partner)); + this.FailOn(() => pawn.Drafted); + this.FailOn(() => Partner.IsFighting()); + + yield return Toils_Reserve.Reserve(iTarget, 1, 0); + int basePrice = WhoringHelper.PriceOfWhore(pawn); + float bedMult = WhoreBed_Utility.CalculatePriceFactor(Bed); + //yield return Toils_Reserve.Reserve(BedInd, Bed.SleepingSlotsCount, 0); + + if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":generate job toils"); + Toil gotoBed = new Toil(); + gotoBed.defaultCompleteMode = ToilCompleteMode.PatherArrival; + gotoBed.FailOnBedNoLongerUsable(iBed, Bed); + gotoBed.AddFailCondition(() => Partner.Downed); + gotoBed.FailOn(() => !Partner.CanReach(Bed, PathEndMode.Touch, Danger.Deadly)); + gotoBed.initAction = delegate + { + if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":gotoWhoreBed"); + pawn.pather.StartPath(SleepSpot, PathEndMode.OnCell); + Partner.jobs.StopAll(); + Job job = JobMaker.MakeJob(JobDefOf.GotoMindControlled, SleepSpot); + Partner.jobs.StartJob(job, JobCondition.InterruptForced); + }; + yield return gotoBed; + + ticks_left = (int)(2000.0f * Rand.Range(0.30f, 1.30f)); + + Toil waitInBed = new Toil(); + waitInBed.initAction = delegate + { + ticksLeftThisToil = 5000; + }; + waitInBed.tickAction = delegate + { + pawn.GainComfortFromCellIfPossible(); + if (IsInOrByBed(Bed, Partner) && pawn.PositionHeld == Partner.PositionHeld) + { + ReadyForNextToil(); + } + }; + waitInBed.defaultCompleteMode = ToilCompleteMode.Delay; + yield return waitInBed; + + Toil StartPartnerJob = new Toil(); + StartPartnerJob.defaultCompleteMode = ToilCompleteMode.Instant; + StartPartnerJob.socialMode = RandomSocialMode.Off; + StartPartnerJob.initAction = delegate + { + if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":StartPartnerJob"); + var gettin_loved = JobMaker.MakeJob(PartnerJob, pawn, Bed); + Partner.jobs.StartJob(gettin_loved, JobCondition.InterruptForced); + }; + yield return StartPartnerJob; + + Toil SexToil = new Toil(); + SexToil.defaultCompleteMode = ToilCompleteMode.Never; + SexToil.socialMode = RandomSocialMode.Off; + SexToil.handlingFacing = true; + SexToil.FailOn(() => Partner.Dead); + SexToil.FailOn(() => Partner.CurJob.def != PartnerJob); + SexToil.initAction = delegate + { + if (WhoringBase.DebugWhoring) ModLog.Message("" + this.GetType().ToString() + ":SexToil start"); + + // refresh bed reservation + Bed.ReserveForWhoring(pawn, ticks_left+100); + + Start(); + + // TODO: replace this quick n dirty way + CondomUtility.GetCondomFromRoom(pawn); + // Try to use whore's condom first, then client's + Sexprops.usedCondom = CondomUtility.TryUseCondom(pawn) || CondomUtility.TryUseCondom(Partner); + + if (!RJWSettings.HippieMode && xxx.HasNonPolyPartner(Partner, true)) + { + Pawn lover = LovePartnerRelationUtility.ExistingLovePartner(Partner); + // We have to do a few other checks because the pawn might have multiple lovers and ExistingLovePartner() might return the wrong one + if (lover != null && pawn != lover && !lover.Dead && (lover.Map == Partner.Map || Rand.Value < 0.25) && GenSight.LineOfSight(lover.Position, Partner.Position, lover.Map)) + { + lover.needs.mood.thoughts.memories.TryGainMemory(RimWorld.ThoughtDefOf.CheatedOnMe, Partner); + } + } + }; + SexToil.AddPreTickAction(delegate + { + if (pawn.IsHashIntervalTick(ticks_between_hearts)) + if (xxx.is_nympho(pawn)) + FleckMaker.ThrowMetaIcon(pawn.Position, pawn.Map, FleckDefOf.Heart); + else + FleckMaker.ThrowMetaIcon(pawn.Position, pawn.Map, xxx.mote_noheart); + SexTick(pawn, Partner); + SexUtility.reduce_rest(Partner, 1); + SexUtility.reduce_rest(pawn, 2); + if (ticks_left % 100 == 0) + Bed.ReserveForWhoring(pawn, ticks_left + 100); // without this, reservation sometimes expires before sex is finished + if (ticks_left <= 0) + ReadyForNextToil(); + }); + SexToil.AddFinishAction(delegate + { + End(); + }); + yield return SexToil; + + Toil afterSex = new Toil + { + initAction = delegate + { + // Adding interactions, social logs, etc + SexUtility.ProcessSex(Sexprops); + + Bed.UnreserveForWhoring(); + + if (!(Partner.IsColonist && (pawn.IsPrisonerOfColony || pawn.IsColonist))) + { + int netPrice = (int) (basePrice * bedMult); + if (netPrice == 0) + netPrice += 1; + + int bedTip = netPrice - basePrice; + int defect = WhoringHelper.PayPriceToWhore(Partner, netPrice, pawn); + + if (WhoringBase.DebugWhoring) + { + ModLog.Message($"{GetType()}:afterSex toil - {Partner} tried to pay {basePrice}(whore price) + {bedTip}(room modifier) silver to {pawn}"); + + if (defect <= 0) + ModLog.Message(" Paid full price"); + else if (defect <= bedTip) + ModLog.Message(" Could not pay full tip"); + else + ModLog.Message(" Failed to pay base price"); + } + + WhoringHelper.UpdateRecords(pawn, netPrice - defect); + } + + if (SexUtility.ConsiderCleaning(pawn)) + { + LocalTargetInfo cum = pawn.PositionHeld.GetFirstThing(pawn.Map); + + Job clean = JobMaker.MakeJob(JobDefOf.Clean); + clean.AddQueuedTarget(TargetIndex.A, cum); + + pawn.jobs.jobQueue.EnqueueFirst(clean); + } + }, + defaultCompleteMode = ToilCompleteMode.Instant + }; + yield return afterSex; + } + } +} diff --git a/1.4/Source/Mod/JobGivers/JobGiver_WhoreInvitingVisitors.cs b/1.4/Source/Mod/JobGivers/JobGiver_WhoreInvitingVisitors.cs new file mode 100644 index 0000000..c438e29 --- /dev/null +++ b/1.4/Source/Mod/JobGivers/JobGiver_WhoreInvitingVisitors.cs @@ -0,0 +1,260 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using rjw; +using Verse; +using Verse.AI; +//using Multiplayer.API; + +namespace rjwwhoring +{ + public class JobGiver_WhoreInvitingVisitors : ThinkNode_JobGiver + { + // Checks if pawn has a memory. + // Maybe not the best place for function, might be useful elsewhere too. + public static bool MemoryChecker(Pawn pawn, ThoughtDef thought) + { + Thought_Memory val = pawn.needs.mood.thoughts.memories.Memories.Find((Thought_Memory x) => (object)x.def == thought); + return val == null ? false : true; + } + + //[SyncMethod] + private static bool Roll_to_skip(Pawn client, Pawn whore) + { + //Rand.PopState(); + //Rand.PushState(RJW_Multiplayer.PredictableSeed()); + float fuckability = SexAppraiser.would_fuck(client, whore); // 0.0 to 1. + + // More likely to skip other whores, because they're supposed to be working. + if (client.IsDesignatedService()) + fuckability *= 0.6f; + if (WhoringBase.ClientAlwaysAccept) + { + return fuckability >= 0.1f && xxx.need_some_sex(client) >= 1f; + } + else + return fuckability >= 0.1f && xxx.need_some_sex(client) >= 1f && Rand.Chance(0.5f); + } + + /* + public static Pawn Find_pawn_to_fuck(Pawn whore, Map map) + { + Pawn best_fuckee = null; + float best_distance = 1.0e6f; + foreach (Pawn q in map.mapPawns.FreeColonists) + if ((q != whore) && + xxx.need_some_sex(q)>0 && + whore.CanReserve(q, 1, 0) && + q.CanReserve(whore, 1, 0) && + Roll_to_skip(whore, q) && + (!q.Position.IsForbidden(whore)) && + xxx.is_healthy(q)) + { + var dis = whore.Position.DistanceToSquared(q.Position); + if (dis < best_distance) + { + best_fuckee = q; + best_distance = dis; + } + } + return best_fuckee; + } + */ + + private sealed class FindAttractivePawnHelper + { + internal Pawn whore; + + internal bool TraitCheckFail(Pawn client) + { + if (!xxx.is_human(client)) + return true; + if (!xxx.has_traits(client)) + return true; + if (!(xxx.can_fuck(client) || xxx.can_be_fucked(client)) || !xxx.IsTargetPawnOkay(client)) + return true; + + //Log.Message("client:" + client + " whore:" + whore); + if (CompRJW.CheckPreference(client, whore) == false) + return true; + return false; // Everything ok. + } + + //Use this check when client is not in the same faction as the whore + internal bool FactionCheckPass(Pawn client) + { + return ((client.Map == whore.Map) && (client.Faction != null && client.Faction != whore.Faction) && !client.IsPrisoner && !xxx.is_slave(client) && !client.HostileTo(whore)); + } + + //Use this check when client is in the same faction as the whore + //[SyncMethod] + internal bool RelationCheckPass(Pawn client) + { + //Rand.PopState(); + //Rand.PushState(RJW_Multiplayer.PredictableSeed()); + if (xxx.isSingleOrPartnerNotHere(client) || xxx.is_lecher(client) || Rand.Value < 0.9f) + { + if (client != LovePartnerRelationUtility.ExistingLovePartner(whore)) + { //Exception for prisoners to account for PrisonerWhoreSexualEmergencyTree, which allows prisoners to try to hook up with anyone who's around (mostly other prisoners or warden) + return (client != whore) & (client.Map == whore.Map) && (client.Faction == whore.Faction || whore.IsPrisoner) && (client.IsColonist || whore.IsPrisoner) && WhoringHelper.IsHookupAppealing(whore, client); + } + } + return false; + } + } + + //[SyncMethod] + public static Pawn FindAttractivePawn(Pawn whore, out int price) + { + price = 0; + if (whore == null || xxx.is_asexual(whore)) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(whore)} is asexual, abort"); + return null; + } + //Rand.PopState(); + //Rand.PushState(RJW_Multiplayer.PredictableSeed()); + + FindAttractivePawnHelper client = new FindAttractivePawnHelper + { + whore = whore + }; + price = WhoringHelper.PriceOfWhore(whore); + int priceOfWhore = price; + + IntVec3 pos = whore.Position; + + IEnumerable potentialClients = whore.Map.mapPawns.AllPawnsSpawned; + potentialClients = potentialClients.Where(x + => x != whore + && !x.IsForbidden(whore) + && !x.HostileTo(whore) + && !x.IsPrisoner + && !xxx.is_slave(x) + && !x.IsColonist + && x.Position.DistanceTo(pos) < 100 + && xxx.is_healthy_enough(x)); + + + potentialClients = potentialClients.Except(potentialClients.Where(client.TraitCheckFail)); + + if (!whore.IsPrisoner) + potentialClients = potentialClients.Except(potentialClients.Where(x => !whore.CanReserveAndReach(x, PathEndMode.ClosestTouch, Danger.Some, 1))); + else + potentialClients = potentialClients.Except(potentialClients.Where(x => !x.CanReserveAndReach(whore, PathEndMode.ClosestTouch, Danger.Some, 1))); + + if (!potentialClients.Any()) return null; + + if (WhoringBase.DebugWhoring) ModLog.Message($" FindAttractivePawn number of all potential clients {potentialClients.Count()}"); + + List valid_targets = new List(); + + if (!whore.IsPrisoner) + foreach (Pawn target in potentialClients) + { + if(Pather_Utility.cells_to_target_casual(whore, target.Position)) + if (Pather_Utility.can_path_to_target(whore, target.Position)) + valid_targets.Add(target); + } + else + foreach (Pawn target in potentialClients) + { + if (Pather_Utility.cells_to_target_casual(target, whore.Position)) + if (Pather_Utility.can_path_to_target(target, whore.Position)) + valid_targets.Add(target); + } + + if (WhoringBase.DebugWhoring) ModLog.Message($" number of reachable clients {valid_targets.Count()}"); + + + //IEnumerable guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction + // && WhoringHelper.CanAfford(x, whore, priceOfWhore)); + + //if (RJWSettings.DebugWhoring) ModLog.Message($" number of clients can afford {guestsSpawned.Count()}"); + + //guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction + // && x != LovePartnerRelationUtility.ExistingLovePartner(whore)); + + //if (RJWSettings.DebugWhoring) ModLog.Message($" number of relations OK {guestsSpawned.Count()}"); + //guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction + // && !MemoryChecker(x, ThoughtDef.Named("RJWFailedSolicitation"))); + + //if (RJWSettings.DebugWhoring) ModLog.Message($" number of clients can memory OK {guestsSpawned.Count()}"); + + IEnumerable guestsSpawned = valid_targets.Where(x => x.Faction != whore.Faction + && !MemoryChecker(x, ThoughtDef.Named("RJWFailedSolicitation")) + && WhoringHelper.CanAfford(x, whore, priceOfWhore) + && x != LovePartnerRelationUtility.ExistingLovePartner(whore)); + + if (guestsSpawned.Any()) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" number of all acceptable Guests {guestsSpawned.Count()}"); + return guestsSpawned.RandomElement(); + } + + return null; + + //use casual sex for colonist hooking + if (WhoringBase.DebugWhoring) ModLog.Message($" found no guests, trying colonists"); + + if (!WhoringHelper.WillPawnTryHookup(whore))// will hookup colonists? + { + return null; + } + IEnumerable freeColonists = valid_targets.Where(x => x.Faction == whore.Faction + && Roll_to_skip(x, whore)); + + if (WhoringBase.DebugWhoring) ModLog.Message($" number of free colonists {freeColonists.Count()}"); + + freeColonists = freeColonists.Where(x => client.RelationCheckPass(x) && !MemoryChecker(x, ThoughtDef.Named("RJWTurnedDownWhore"))); + + if (freeColonists.Any()) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" number of all acceptable Colonists {freeColonists.Count()}"); + return freeColonists.RandomElement(); + } + + return null; + } + + protected override Job TryGiveJob(Pawn pawn) + { + + // Most things are now checked in the ThinkNode_ConditionalWhore. + + if (pawn.Drafted) return null; + //if (MP.IsInMultiplayer) return null; //fix error someday, maybe + + if (pawn.jobs.curDriver is JobDriver_Sex || pawn.jobs.curDriver is JobDriver_WhoreInvitingVisitors) return null; // already having sex + + if (!SexUtility.ReadyForLovin(pawn)) + { + //Whores need rest too, but this'll reduxe the wait a bit every it triggers. + pawn.mindState.canLovinTick -= 40; + return null; + } + + if (WhoringBase.DebugWhoring) ModLog.Message($"JobGiver_WhoreInvitingVisitors.TryGiveJob:({xxx.get_pawnname(pawn)})"); + + int price; + Pawn client = FindAttractivePawn(pawn, out price); + if (client == null) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" no clients found"); + return null; + } + + if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(client)} is client"); + + Building_Bed whorebed = WhoreBed_Utility.FindBed(pawn, client); + if (whorebed == null) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(pawn)} + {xxx.get_pawnname(client)} - no usable bed found"); + return null; + } + whorebed.ReserveForWhoring(pawn, 600); // reserve for a short while until whore can actually ask customer + + return JobMaker.MakeJob(xxx.whore_inviting_visitors, client, whorebed); + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/Location/Brothel_Room.cs b/1.4/Source/Mod/Location/Brothel_Room.cs new file mode 100644 index 0000000..27921c3 --- /dev/null +++ b/1.4/Source/Mod/Location/Brothel_Room.cs @@ -0,0 +1,28 @@ +using Verse; +using RimWorld; + + +namespace rjwwhoring +{ + + public class RoomRoleWorker_Brothel : RoomRoleWorker + { + public override float GetScore(Room room) + { + int num = 0; + var allContainedThings = room.ContainedAndAdjacentThings; + foreach (var thing in allContainedThings) + { + var building_Bed = thing as Building_Bed; + if (building_Bed?.def.building.bed_humanlike == true) + { + if (building_Bed.ForPrisoners) return 0; + if (building_Bed.IsAllowedForWhoringAll()) num++; + } + } + if (num < 1) return 0; + return num * 110001; // higher than guest beds or "regular" beds when counting barracks + } + } + +} \ No newline at end of file diff --git a/1.4/Source/Mod/ThinkTreeNodes/ThinkNode_ChancePerHour_Whore.cs b/1.4/Source/Mod/ThinkTreeNodes/ThinkNode_ChancePerHour_Whore.cs new file mode 100644 index 0000000..0239322 --- /dev/null +++ b/1.4/Source/Mod/ThinkTreeNodes/ThinkNode_ChancePerHour_Whore.cs @@ -0,0 +1,59 @@ +namespace rjwwhoring +{ + //This class is not used now. + /* + public class ThinkNode_ChancePerHour_Whore : ThinkNode_ChancePerHour + { + protected override float MtbHours(Pawn pawn) + { + // Use the fappin mtb hours as the base number b/c it already accounts for things like age + var base_mtb = xxx.config.whore_mtbh_mul * ThinkNode_ChancePerHour_Fappin.get_fappin_mtb_hours(pawn); + if (base_mtb < 0.0f) + return -1.0f; + + float desire_factor; + { + var need_sex = pawn.needs.TryGetNeed(); + if (need_sex != null) + { + if (need_sex.CurLevel <= need_sex.thresh_frustrated()) + desire_factor = 0.15f; + else if (need_sex.CurLevel <= need_sex.thresh_horny()) + desire_factor = 0.60f; + else + desire_factor = 1.00f; + } + else + desire_factor = 1.00f; + } + + float personality_factor; + { + personality_factor = 1.0f; + if (pawn.story != null) + { + foreach (var trait in pawn.story.traits.allTraits) + { + if (trait.def == xxx.nymphomaniac) personality_factor *= 0.25f; + else if (trait.def == TraitDefOf.Greedy) personality_factor *= 0.50f; + else if (xxx.RomanceDiversifiedIsActive&&(trait.def==xxx.philanderer || trait.def == xxx.polyamorous)) personality_factor *= 0.75f; + else if (xxx.RomanceDiversifiedIsActive && (trait.def == xxx.faithful)&& LovePartnerRelationUtility.HasAnyLovePartner(pawn)) personality_factor *= 30f; + } + } + } + + float fun_factor; + { + if ((pawn.needs.joy != null) && (xxx.is_nympho(pawn))) + fun_factor = Mathf.Clamp01(0.50f + pawn.needs.joy.CurLevel); + else + fun_factor = 1.00f; + } + + var gender_factor = (pawn.gender == Gender.Male) ? 1.0f : 3.0f; + + return base_mtb * desire_factor * personality_factor * fun_factor * gender_factor; + } + } + */ +} \ No newline at end of file diff --git a/1.4/Source/Mod/ThinkTreeNodes/ThinkNode_ConditionalWhore.cs b/1.4/Source/Mod/ThinkTreeNodes/ThinkNode_ConditionalWhore.cs new file mode 100644 index 0000000..fcfa9d5 --- /dev/null +++ b/1.4/Source/Mod/ThinkTreeNodes/ThinkNode_ConditionalWhore.cs @@ -0,0 +1,25 @@ +using Verse; +using Verse.AI; +using RimWorld; +using rjw; + +namespace rjwwhoring +{ + /// + /// Whore/prisoner look for customers + /// + public class ThinkNode_ConditionalWhore : ThinkNode_Conditional + { + protected override bool Satisfied(Pawn p) + { + // No animal whorin' for now. + if (xxx.is_animal(p)) + return false; + + if (!InteractionUtility.CanInitiateInteraction(p)) + return false; + + return xxx.is_whore(p); + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/Thoughts/ThoughtWorker_Whore.cs b/1.4/Source/Mod/Thoughts/ThoughtWorker_Whore.cs new file mode 100644 index 0000000..cbe88f9 --- /dev/null +++ b/1.4/Source/Mod/Thoughts/ThoughtWorker_Whore.cs @@ -0,0 +1,43 @@ +using RimWorld; +using Verse; +using System.Collections.Generic; + +namespace rjwwhoring +{ + /// + /// Extends the standard thought to add a counter for the whore stages + /// + public class ThoughtDef_Whore : ThoughtDef + { + public List stageCounts = new List(); + public int storyOffset = 0; + } + + public class ThoughtWorker_Whore : Thought_Memory + { + public static readonly HashSet backstories = new HashSet(DefDatabase.GetNamed("WhoreBackstories").strings); + + protected List Stages => ((ThoughtDef_Whore) def).stageCounts; + protected int StoryOffset => ((ThoughtDef_Whore) def).storyOffset; + + public override int CurStageIndex + { + get + { + int timesWhored = pawn.records.GetAsInt(RecordDefOf.CountOfWhore); + + if (backstories.Contains(pawn.story?.Adulthood?.titleShort)) + { + timesWhored += StoryOffset; + } + + if (timesWhored > Stages[Stages.Count - 1]) + { + return Stages.Count - 1; + } + + return Stages.FindLastIndex(v => timesWhored > v) + 1; + } + } + } +} diff --git a/1.4/Source/Mod/Whoring.csproj b/1.4/Source/Mod/Whoring.csproj new file mode 100644 index 0000000..b213640 --- /dev/null +++ b/1.4/Source/Mod/Whoring.csproj @@ -0,0 +1,112 @@ + + + + + Debug + AnyCPU + {3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28} + Library + rjwwhoring + RimJobWorldWhoring + v4.7.2 + 512 + + + AnyCPU + true + full + false + ..\..\Assemblies\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\..\Assemblies\ + TRACE + prompt + 4 + + + + ..\packages\Lib.Harmony.2.2.2\lib\net472\0Harmony.dll + False + + + ..\..\..\..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll + False + + + ..\..\..\..\..\..\..\workshop\content\294100\818773962\v1.4\Assemblies\HugsLib.dll + False + + + ..\..\..\..\rjw\1.4\Assemblies\RJW.dll + False + + + + + ..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll + False + + + ..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll + False + + + ..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.InputLegacyModule.dll + False + + + ..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.InputModule.dll + False + + + ..\..\..\..\..\RimWorldWin64_Data\Managed\UnityEngine.TextRenderingModule.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/1.4/Source/Mod/WhoringBase.cs b/1.4/Source/Mod/WhoringBase.cs new file mode 100644 index 0000000..0edb576 --- /dev/null +++ b/1.4/Source/Mod/WhoringBase.cs @@ -0,0 +1,71 @@ +using System; +using HugsLib; +using HugsLib.Settings; +using RimWorld; +using Verse; + +namespace rjwwhoring +{ + public class WhoringBase : ModBase + { + public override string ModIdentifier + { + get + { + return "RJW_Whoring"; + } + } + + public static DataStore DataStore;//reference to savegame data, hopefully + + public override void SettingsChanged() + { + //ToggleTabIfNeeded(); + } + public override void WorldLoaded() + { + DataStore = Find.World.GetComponent(); + //ToggleTabIfNeeded(); + } + + private void ToggleTabIfNeeded() + { + //DefDatabase.GetNamed("RJW_Brothel").buttonVisible = whoringtab_enabled; + } + + //public static SettingHandle whoringtab_enabled; + public static SettingHandle show_whore_price_factor_on_bed; + public static SettingHandle show_whore_widgets_on_bed; + public static SettingHandle DebugWhoring; + public static SettingHandle MoneyPrinting; + public static SettingHandle ClientAlwaysAccept; + + public override void DefsLoaded() + { + //whoringtab_enabled = Settings.GetHandle("whoringtab_enabled", + // "whoringtab_enabled".Translate(), + // "whoringtab_enabled_desc".Translate(), + // true); + show_whore_price_factor_on_bed = Settings.GetHandle("show_whore_price_factor_on_bed", + "show_whore_price_factor_on_bed".Translate(), + "show_whore_price_factor_on_bed_desc".Translate(), + false); + show_whore_widgets_on_bed = Settings.GetHandle("show_whore_widgets_on_bed", + "show_whore_widgets_on_bed".Translate(), + "show_whore_widgets_on_bed_desc".Translate(), + false); + MoneyPrinting = Settings.GetHandle("MoneyPrinting", + "MoneyPrinting".Translate(), + "MoneyPrinting_desc".Translate(), + false); + ClientAlwaysAccept = Settings.GetHandle("ClientAlwaysAccept", + "ClientAlwaysAccept".Translate(), + "ClientAlwaysAccept_desc".Translate(), + false); + DebugWhoring = Settings.GetHandle("DebugWhoring", + "DebugWhoring".Translate(), + "DebugWhoring_desc".Translate(), + false); + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnCheckbox_Whore.cs b/1.4/Source/Mod/WhoringTab/PawnColumnCheckbox_Whore.cs new file mode 100644 index 0000000..72bb337 --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnCheckbox_Whore.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Verse; +using UnityEngine; +using RimWorld; +using Verse.Sound; +using rjw; + +namespace rjwwhoring.MainTab +{ + public abstract class PawnColumnCheckbox_Whore : PawnColumnWorker + { + public const int HorizontalPadding = 2; + + public override void DoCell(Rect rect, Pawn pawn, PawnTable table) + { + if (!this.HasCheckbox(pawn)) + { + return; + } + int num = (int)((rect.width - 24f) / 2f); + int num2 = Mathf.Max(3, 0); + Vector2 vector = new Vector2(rect.x + (float)num, rect.y + (float)num2); + Rect rect2 = new Rect(vector.x, vector.y, 24f, 24f); + if (Find.TickManager.TicksGame % 60 == 0) + { + pawn.UpdatePermissions(); + //Log.Message("GetDisabled UpdateCanDesignateService for " + xxx.get_pawnname(pawn)); + //Log.Message("UpdateCanDesignateService " + pawn.UpdateCanDesignateService()); + //Log.Message("CanDesignateService " + pawn.CanDesignateService()); + //Log.Message("GetDisabled " + GetDisabled(pawn)); + } + bool disabled = this.GetDisabled(pawn); + bool value; + if (disabled) + { + value = false; + } + else + { + value = this.GetValue(pawn); + } + + bool flag = value; + Vector2 topLeft = vector; + WhoreCheckbox.Checkbox(topLeft, ref value, 24f, disabled, WhoreCheckbox.WhoreCheckboxOnTex, WhoreCheckbox.WhoreCheckboxOffTex, WhoreCheckbox.WhoreCheckboxDisabledTex); + if (Mouse.IsOver(rect2)) + { + string tip = this.GetTip(pawn); + if (!tip.NullOrEmpty()) + { + TooltipHandler.TipRegion(rect2, tip); + } + } + if (value != flag) + { + this.SetValue(pawn, value); + } + } + + public override int GetMinWidth(PawnTable table) + { + return Mathf.Max(base.GetMinWidth(table), 28); + } + + public override int GetMaxWidth(PawnTable table) + { + return Mathf.Min(base.GetMaxWidth(table), this.GetMinWidth(table)); + } + + public override int GetMinCellHeight(Pawn pawn) + { + return Mathf.Max(base.GetMinCellHeight(pawn), 24); + } + + public override int Compare(Pawn a, Pawn b) + { + return this.GetValueToCompare(a).CompareTo(this.GetValueToCompare(b)); + } + + private int GetValueToCompare(Pawn pawn) + { + if (!this.HasCheckbox(pawn)) + { + return 0; + } + if (!this.GetValue(pawn)) + { + return 1; + } + return 2; + } + + protected virtual string GetTip(Pawn pawn) + { + return null; + } + + protected virtual bool HasCheckbox(Pawn pawn) + { + return true; + } + + protected abstract bool GetValue(Pawn pawn); + + protected abstract void SetValue(Pawn pawn, bool value); + + protected abstract bool GetDisabled(Pawn pawn); + + protected override void HeaderClicked(Rect headerRect, PawnTable table) + { + base.HeaderClicked(headerRect, table); + if (Event.current.shift) + { + List pawnsListForReading = table.PawnsListForReading; + for (int i = 0; i < pawnsListForReading.Count; i++) + { + if (this.HasCheckbox(pawnsListForReading[i])) + { + if (Event.current.button == 0) + { + if (!this.GetValue(pawnsListForReading[i])) + { + this.SetValue(pawnsListForReading[i], true); + } + } + else if (Event.current.button == 1 && this.GetValue(pawnsListForReading[i])) + { + this.SetValue(pawnsListForReading[i], false); + } + } + } + if (Event.current.button == 0) + { + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); + } + else if (Event.current.button == 1) + { + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); + } + } + } + + protected override string GetHeaderTip(PawnTable table) + { + return base.GetHeaderTip(table) + "\n" + "CheckboxShiftClickTip".Translate(); + } + } +} + diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_AverageMoneyByWhore.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_AverageMoneyByWhore.cs new file mode 100644 index 0000000..8a38a33 --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_AverageMoneyByWhore.cs @@ -0,0 +1,29 @@ +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_AverageMoneyByWhore : PawnColumnWorker_TextCenter + { + protected override string GetTextFor(Pawn pawn) + { + return ((int)GetValueToCompare(pawn)).ToString(); + } + + public override int Compare(Pawn a, Pawn b) + { + return GetValueToCompare(a).CompareTo(GetValueToCompare(b)); + } + + private float GetValueToCompare(Pawn pawn) + { + float total = pawn.records.GetValue(RecordDefOf.EarnedMoneyByWhore); + float count = pawn.records.GetValue(RecordDefOf.CountOfWhore); + if ((int)count == 0) + { + return 0; + } + return (total / count); + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_CountOfWhore.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_CountOfWhore.cs new file mode 100644 index 0000000..0e4cf05 --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_CountOfWhore.cs @@ -0,0 +1,23 @@ +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_CountOfWhore : PawnColumnWorker_TextCenter + { + protected override string GetTextFor(Pawn pawn) + { + return GetValueToCompare(pawn).ToString(); + } + + public override int Compare(Pawn a, Pawn b) + { + return GetValueToCompare(a).CompareTo(GetValueToCompare(b)); + } + + private int GetValueToCompare(Pawn pawn) + { + return pawn.records.GetAsInt(RecordDefOf.CountOfWhore); + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_EarnedMoneyByWhore.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_EarnedMoneyByWhore.cs new file mode 100644 index 0000000..3665822 --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_EarnedMoneyByWhore.cs @@ -0,0 +1,23 @@ +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_EarnedMoneyByWhore : PawnColumnWorker_TextCenter + { + protected override string GetTextFor(Pawn pawn) + { + return GetValueToCompare(pawn).ToString(); + } + + public override int Compare(Pawn a, Pawn b) + { + return GetValueToCompare(a).CompareTo(GetValueToCompare(b)); + } + + private int GetValueToCompare(Pawn pawn) + { + return pawn.records.GetAsInt(RecordDefOf.EarnedMoneyByWhore); + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_IsWhore.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_IsWhore.cs new file mode 100644 index 0000000..cf02240 --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_IsWhore.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using RimWorld; +using RimWorld.Planet; +using rjw; +using UnityEngine; +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_IsWhore : PawnColumnCheckbox_Whore + { + protected override bool GetDisabled(Pawn pawn) + { + return !pawn.CanDesignateService(); + } + + protected override bool GetValue(Pawn pawn) + { + return pawn.IsDesignatedService() && xxx.is_human(pawn); + } + + protected override void SetValue(Pawn pawn, bool value) + { + if (value == this.GetValue(pawn)) return; + + pawn.ToggleService(); + } + /* + private static readonly Texture2D serviceOn = ContentFinder.Get("UI/Tab/Service_on"); + private static readonly Texture2D serviceOff = ContentFinder.Get("UI/Tab/Service_off"); + + protected override Texture2D GetIconFor(Pawn pawn) + { + return pawn.IsDesignatedService() ? serviceOn : null; + }*/ + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_Mood.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_Mood.cs new file mode 100644 index 0000000..f4a6f6b --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_Mood.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_Mood : PawnColumnWorker_TextCenter + { + protected override string GetTextFor(Pawn pawn) + { + return GetValueToCompare(pawn).ToStringPercent(); + } + + public override int Compare(Pawn a, Pawn b) + { + return GetValueToCompare(a).CompareTo(GetValueToCompare(b)); + } + + private float GetValueToCompare(Pawn pawn) + { + return pawn.needs.mood.CurLevelPercentage; + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_PriceRangeOfWhore.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_PriceRangeOfWhore.cs new file mode 100644 index 0000000..220713b --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_PriceRangeOfWhore.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_PriceRangeOfWhore : PawnColumnWorker_TextCenter + { + protected internal int min; + protected internal int max; + + protected override string GetTextFor(Pawn pawn) + { + min = WhoringHelper.WhoreMinPrice(pawn); + max = WhoringHelper.WhoreMaxPrice(pawn); + return string.Format("{0} - {1}", min, max); + } + + public override int Compare(Pawn a, Pawn b) + { + return GetValueToCompare(a).CompareTo(GetValueToCompare(b)); + } + + protected override string GetTip(Pawn pawn) + { + string minPriceTip = string.Format( + " Base: {0}\n Traits: {1}", + WhoringHelper.baseMinPrice, + (WhoringHelper.WhoreTraitAdjustmentMin(pawn) -1f).ToStringPercent() + ); + string maxPriceTip = string.Format( + " Base: {0}\n Traits: {1}", + WhoringHelper.baseMaxPrice, + (WhoringHelper.WhoreTraitAdjustmentMax(pawn) -1f).ToStringPercent() + ); + string bothTip = string.Format( + " Gender: {0}\n Age: {1}\n Injuries: {2}", + (WhoringHelper.WhoreGenderAdjustment(pawn) - 1f).ToStringPercent(), + (WhoringHelper.WhoreAgeAdjustment(pawn) - 1f).ToStringPercent(), + (WhoringHelper.WhoreInjuryAdjustment(pawn) - 1f).ToStringPercent() + ); + return string.Format("Min:\n{0}\nMax:\n{1}\nBoth:\n{2}", minPriceTip, maxPriceTip, bothTip); + } + + private int GetValueToCompare(Pawn pawn) + { + return min; + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_TextCenter.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_TextCenter.cs new file mode 100644 index 0000000..f72193f --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_TextCenter.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace rjwwhoring.MainTab +{ + public abstract class PawnColumnWorker_TextCenter : PawnColumnWorker_Text + { + public override void DoCell(Rect rect, Pawn pawn, PawnTable table) + { + Rect rect2 = new Rect(rect.x, rect.y, rect.width, Mathf.Min(rect.height, 30f)); + string textFor = GetTextFor(pawn); + if (textFor != null) + { + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.MiddleCenter; + Text.WordWrap = false; + Widgets.Label(rect2, textFor); + Text.WordWrap = true; + Text.Anchor = TextAnchor.UpperLeft; + string tip = GetTip(pawn); + if (!tip.NullOrEmpty()) + { + TooltipHandler.TipRegion(rect2, tip); + } + } + } + } +} diff --git a/1.4/Source/Mod/WhoringTab/PawnColumnWorker_WhoreExperience.cs b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_WhoreExperience.cs new file mode 100644 index 0000000..c0d85ba --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnColumnWorker_WhoreExperience.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Verse; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public class PawnColumnWorker_WhoreExperience : PawnColumnWorker_TextCenter + { + public static readonly HashSet backstories = new HashSet(DefDatabase.GetNamed("WhoreBackstories").strings); + + protected override string GetTextFor(Pawn pawn) + { + + int b = backstories.Contains(pawn.story?.Adulthood?.titleShort) ? 30 : 0; + int score = pawn.records.GetAsInt(RecordDefOf.CountOfWhore); + return (score + b).ToString(); + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/WhoringTab/PawnTable_Whores.cs b/1.4/Source/Mod/WhoringTab/PawnTable_Whores.cs new file mode 100644 index 0000000..3a25bb4 --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/PawnTable_Whores.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using rjw; +using Verse; + +namespace rjwwhoring.MainTab +{ + public class PawnTable_Whores : PawnTable_PlayerPawns + { + public PawnTable_Whores(PawnTableDef def, Func> pawnsGetter, int uiWidth, int uiHeight) : base(def, pawnsGetter, uiWidth, uiHeight) { } + + //default sorting + protected override IEnumerable LabelSortFunction(IEnumerable input) + { + //return input.OrderBy(p => p.Name); + foreach (Pawn p in input) + p.UpdatePermissions(); + return input.OrderByDescending(p => (p.IsPrisonerOfColony || p.IsSlaveOfColony) != false).ThenBy(p => xxx.get_pawnname(p)); + //return input.OrderByDescending(p => (p.IsPrisonerOfColony || p.IsSlaveOfColony) != false).ThenBy(p => (p.Name.ToStringShort.Colorize(Color.yellow))); + //return input.OrderBy(p => xxx.get_pawnname(p)); + } + + protected override IEnumerable PrimarySortFunction(IEnumerable input) + { + foreach (Pawn p in input) + p.UpdatePermissions(); + return input; + //return base.PrimarySortFunction(input); + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/WhoringTab/WhoreCheckbox.cs b/1.4/Source/Mod/WhoringTab/WhoreCheckbox.cs new file mode 100644 index 0000000..1f21bdb --- /dev/null +++ b/1.4/Source/Mod/WhoringTab/WhoreCheckbox.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Verse; +using UnityEngine; +using RimWorld; +using Verse.Sound; + +namespace rjwwhoring.MainTab +{ + [StaticConstructorOnStartup] + public static class WhoreCheckbox + { + public static readonly Texture2D WhoreCheckboxOnTex = ContentFinder.Get("UI/Commands/Service_on"); + public static readonly Texture2D WhoreCheckboxOffTex = ContentFinder.Get("UI/Commands/Service_off"); + public static readonly Texture2D WhoreCheckboxDisabledTex = ContentFinder.Get("UI/Commands/Service_Refuse"); + + private static bool checkboxPainting; + private static bool checkboxPaintingState; + + public static void Checkbox(Vector2 topLeft, ref bool checkOn, float size = 24f, bool disabled = false, Texture2D texChecked = null, Texture2D texUnchecked = null, Texture2D texDisabled = null) + { + WhoreCheckbox.Checkbox(topLeft.x, topLeft.y, ref checkOn, size, disabled, texChecked, texUnchecked); + } + + public static void Checkbox(float x, float y, ref bool checkOn, float size = 24f, bool disabled = false, Texture2D texChecked = null, Texture2D texUnchecked = null, Texture2D texDisabled = null) + { + Rect rect = new Rect(x, y, size, size); + WhoreCheckbox.CheckboxDraw(x, y, checkOn, disabled, size, texChecked, texUnchecked,texDisabled); + if (!disabled) + { + MouseoverSounds.DoRegion(rect); + bool flag = false; + Widgets.DraggableResult draggableResult = Widgets.ButtonInvisibleDraggable(rect, false); + if (draggableResult == Widgets.DraggableResult.Pressed) + { + checkOn = !checkOn; + flag = true; + } + else if (draggableResult == Widgets.DraggableResult.Dragged) + { + checkOn = !checkOn; + flag = true; + WhoreCheckbox.checkboxPainting = true; + WhoreCheckbox.checkboxPaintingState = checkOn; + } + if (Mouse.IsOver(rect) && WhoreCheckbox.checkboxPainting && Input.GetMouseButton(0) && checkOn != WhoreCheckbox.checkboxPaintingState) + { + checkOn = WhoreCheckbox.checkboxPaintingState; + flag = true; + } + if (flag) + { + if (checkOn) + { + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); + } + else + { + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); + } + } + } + } + + private static void CheckboxDraw(float x, float y, bool active, bool disabled, float size = 24f, Texture2D texChecked = null, Texture2D texUnchecked = null, Texture2D texDisabled = null) + { + Texture2D image; + if (disabled) + { + image = ((!(texDisabled != null)) ? WhoreCheckbox.WhoreCheckboxDisabledTex : texDisabled); + } + else if (active) + { + image = ((!(texChecked != null)) ? WhoreCheckbox.WhoreCheckboxOnTex : texChecked); + } + else + { + image = ((!(texUnchecked != null)) ? WhoreCheckbox.WhoreCheckboxOffTex : texUnchecked); + } + Rect position = new Rect(x, y, size, size); + GUI.DrawTexture(position, image); + } + + } +} diff --git a/1.4/Source/Mod/Whoring_Bed_Utilities.cs b/1.4/Source/Mod/Whoring_Bed_Utilities.cs new file mode 100644 index 0000000..a93a898 --- /dev/null +++ b/1.4/Source/Mod/Whoring_Bed_Utilities.cs @@ -0,0 +1,371 @@ +using Verse; +using Verse.AI; +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using System; +using UnityEngine; +using rjw; + +namespace rjwwhoring +{ + public static class WhoreBed_Utility + { + public static readonly RoomRoleDef roleDefBrothel = DefDatabase.GetNamed("Brothel"); + + + // find the best bed for a customer; whore is needed as parameter to only select beds that are reachable + public static Building_Bed FindBed(Pawn whore, Pawn customer) + { + List b = FindReachableAndAvailableWhoreBeds(whore, customer); + return GetBestBedForCustomer(customer, b); + } + public static bool CanUseForWhoring(Pawn pawn, Building_Bed bed) + { + bool flag = bed.IsAvailableForWhoring(pawn) && pawn.CanReserveAndReach(bed, PathEndMode.InteractionCell, Danger.Unspecified) && !bed.IsForbidden(pawn); + return flag; + } + + public static Building_Bed GetBestBedForCustomer(Pawn customer, List beds) + { + if (beds != null && beds.Any()) + { + return beds.MaxBy(bed => CalculateBedScore(customer, bed)); + } + else + { + return null; + } + } + public static float GetCheapestBedFactor(Pawn whore, Pawn customer) + { + List beds = FindReachableAndAvailableWhoreBeds(whore, customer); + return GetCheapestBedFactor(beds); + } + public static float GetCheapestBedFactor(List beds) + { + if (beds != null && beds.Any()) + { + return CalculatePriceFactor(beds.MinBy(bed => CalculatePriceFactor(bed))); + } + else + { + return 0f; + } + } + // unused + /*public static float GetMostExpensiveBedFactor(List beds) + { + if (beds != null && beds.Any()) + { + return CalculatePriceFactor(beds.MaxBy(bed => CalculatePriceFactor(bed))); + } + else + { + return 0f; + } + }//*/ + + public static float CalculateRoomFactor(Room room, int num_humanlike_beds) + { + if (room == null || room.Role == RoomRoleDefOf.None || room.OutdoorsForWork) + return 0.1f; + + float room_multiplier = 1f; + + if (room.Role == roleDefBrothel) + { + room_multiplier *= (float)Math.Pow(0.8, num_humanlike_beds - 1); + } + else // if(room.Role == RoomRoleDefOf.Barracks) + { + room_multiplier /= 2 * (num_humanlike_beds - 1) + 1; + } + + int scoreStageIndex = RoomStatDefOf.Impressiveness.GetScoreStageIndex(room.GetStat(RoomStatDefOf.Impressiveness)); + //Room impressiveness factor + //0 < scoreStageIndex < 10 (Last time checked) + //3 is mediocore + room_multiplier *= (float)(scoreStageIndex <= 3 ? .4f + scoreStageIndex * .2f : 1f + .3f * (scoreStageIndex - 3)); + + return Mathf.Max(room_multiplier, 0); + } + + public static float CalculateBedFactorsForRoom(Room room, Building_Bed except_this_bed = null) + { + float room_factor = 0.1f; + + if (room == null) + return room_factor; + + // get eligible beds + IEnumerable humanlike_beds = room.ContainedBeds.Where(b => b.def.building.bed_humanlike); + int num_humanlike_beds = humanlike_beds.Count(); + if (num_humanlike_beds <= 0) + { + return room_factor; + } + IEnumerable whoring_beds = humanlike_beds.Where(b => b.IsAllowedForWhoringOwner()); + + if (whoring_beds.Any()) + { + // if beds exist, calculate room score + room_factor = CalculateRoomFactor(room, num_humanlike_beds); + + // and update all beds + foreach (Building_Bed b in whoring_beds) + { + // except the bed given as parameter (will be calculated in that bed's function) + if (except_this_bed == null || b.thingIDNumber != except_this_bed.thingIDNumber) + CalculatePriceFactor(b, room_factor); + } + } + return room_factor; + } + + public static void ResetTicksUntilUpdate(Room room) + { + IEnumerable whoring_beds = room.ContainedBeds.Where(b => b.IsAllowedForWhoringOwner()); + + foreach (Building_Bed bed in whoring_beds) + { + // set all to 0 + // if one is needed, it updates all the other beds + // if none is needed, it doesn't matter + // only setting one bed to update has the risk that a different bed's value is required that doesn't trigger an update + WhoringBase.DataStore.GetBedData(bed).scoreUpdateTickDelay = 0; + } + } + + public static float CalculatePriceFactor(Building_Bed bed, float room_multiplier = -1f) + { + + BedData saved_bed_data = WhoringBase.DataStore.GetBedData(bed); + + // cache result (no need for saving): if no result available, calculate; otherwise save tick at which it has been calculated. + // additional parameter "room_multiplier" to skip room analysis + if ((room_multiplier == -1 || room_multiplier >= 0 && room_multiplier == saved_bed_data.roomScore) + && saved_bed_data.bedScore >= 0f + && saved_bed_data.lastScoreUpdateTick > + GenTicks.TicksGame - saved_bed_data.scoreUpdateTickDelay) + { + if (room_multiplier >= 0 && saved_bed_data.scoreUpdateTickDelay < 720) + { + // if saved value is used due to unchanged room multiplier, increase recalc delay + saved_bed_data.scoreUpdateTickDelay += 60 + Rand.Int % 10; + } + //Log.Message("[RJW] lastScoreUpdateTick: " + BukkakeBase.DataStore.GetBedData(bed).lastScoreUpdateTick.ToString() + " / TicksGame: "+ GenTicks.TicksGame.ToString()); + return saved_bed_data.bedScore; + } + + if (room_multiplier < 0) + { + Room room = bed.Map != null && bed.Map.regionAndRoomUpdater.Enabled ? bed.GetRoom() : null; + room_multiplier = CalculateBedFactorsForRoom(room, bed); + } + + // uncomfortable beds reduce price, comfortable beds make customers pay a tip + float comfort = bed.GetStatValue(StatDefOf.Comfort); + float price_factor = room_multiplier * comfort; + + // delay recalculation if result is the same as before + // Rand.Int % 10 flattens the spike over time if many beds are toggled at once + if (price_factor == saved_bed_data.bedScore) + { + if (saved_bed_data.scoreUpdateTickDelay < 720) + { + // slowly increase recalculation delay to two seconds on speed 3 + saved_bed_data.scoreUpdateTickDelay += 60 + Rand.Int % 10; + } + } + else + { + // reset recalculation delay + saved_bed_data.scoreUpdateTickDelay = 60 + Rand.Int % 10; + } + + // update bed data + saved_bed_data.lastScoreUpdateTick = GenTicks.TicksGame; + saved_bed_data.bedScore = price_factor; + saved_bed_data.roomScore = room_multiplier; + + // this is quite spammy + //if (RJWSettings.DebugWhoring) + // Log.Message("[RJW]CalculatePriceFactor for bed " + bed.thingIDNumber.ToString() + ": " + // + "room_multiplier (num beds, impressiveness) ("+room_multiplier.ToString() +") * " + // + "comfort (" + comfort.ToString() + ") = " + price_factor.ToString()); + + return price_factor; + } + + // customers would want the best bed + // TODO: price as factor, rebalance + public static float CalculateBedScore(Pawn customer, Building_Bed bed) + { + float basePriceFactor = CalculatePriceFactor(bed); + + // ascetic pawns want the least impressive room + if (customer.story.traits.HasTrait(TraitDefOf.Ascetic)) + { + float comfort = bed.GetStatValue(StatDefOf.Comfort); + basePriceFactor = comfort * comfort / basePriceFactor; // inverse room effects - may be cheap, but should still be comfortable. ascetic isn't masochistic! + if (WhoringBase.DebugWhoring) + Log.Message("[RJW]CalculateBedScore - Customer is ascetic"); + } + + basePriceFactor *= 200; // make a larger number for better distance scaling (and random effect) + + // horny pawns are in a hurry and want a closer bed + int distance = 0; + if (xxx.is_hornyorfrustrated(customer)) + { + distance = (int)bed.Position.DistanceTo(customer.Position); + //if (RJWSettings.DebugWhoring) + // Log.Message("[RJW]CalculateBedScore - Pawn is horny - distance = "+distance.ToString()); + } + + int random_factor = Rand.Int % 100; + float score = basePriceFactor - distance + random_factor; + + if (WhoringBase.DebugWhoring) + Log.Message("[RJW]CalculateBedScore for bed " + bed.thingIDNumber.ToString() + ": " + + "score from price (" + basePriceFactor.ToString() + ") " + + "- distance (" + distance.ToString() + ") " + + "+ randomness (" + random_factor.ToString() + ") " + + "= " + score.ToString()); + + return score; + } + + public static List FindReachableAndAvailableWhoreBeds(Pawn whore, Pawn customer) + { + List wb = new List(); + + wb = whore.MapHeld.GetWhoreBeds().Where(bed => + !bed.IsForbidden(whore) && + !bed.IsForbidden(customer) && + !bed.IsBurning() && + bed.WhoringIsAllowedForPawn(whore) && + bed.IsAvailableForWhoring(whore) && + whore.CanReserveAndReach(bed, PathEndMode.OnCell, Danger.Unspecified) && + customer.CanReach(bed, PathEndMode.OnCell, Danger.Some) + // TODO: price/affordable? + ).ToList(); + if (WhoringBase.DebugWhoring) + Log.Message("[RJW]FindReachableAndAvailableWhoreBeds - found " + wb.Count().ToString() + " beds"); + return wb; + } + + public static IEnumerable GetWhoreBeds(this Map map, Area area = null) + { + if (map == null) return new Building_Bed[0]; + if (area == null) return map.listerBuildings.AllBuildingsColonistOfClass(); + return map.listerBuildings.AllBuildingsColonistOfClass().Where(b => area[b.Position]); + } + + public static bool WhoringIsAllowedForPawn(this Building_Bed bed, Pawn pawn) + { + if (bed.IsAllowedForWhoringAll()) + return true; + if (bed == pawn.ownership.OwnedBed && bed.IsAllowedForWhoringOwner()) + return true; + return false; + } + + public static void SetAllowedForWhoringOwner(this Building_Bed bed, bool isAllowed) + { + if (!isAllowed) + { + // if whoring is disallowed for owner, also disallow for all + bed.SetAllowedForWhoringAll(false); + } + WhoringBase.DataStore.GetBedData(bed).allowedForWhoringOwner = isAllowed; + } + public static void ToggleAllowedForWhoringOwner(this Building_Bed bed) + { + bed.SetAllowedForWhoringOwner(!WhoringBase.DataStore.GetBedData(bed).allowedForWhoringOwner); + } + public static bool IsAllowedForWhoringOwner(this Building_Bed bed) + { + if (!bed.def.building.bed_humanlike || bed.Faction != Faction.OfPlayerSilentFail || bed.Medical || bed.def.defName.Contains("Guest")) + { + return false; + } + if (bed.ForPrisoners) + { + // no toggle on prisoner beds, they may always use their own bed (if they are supposed to whore, anyway) + return true; + } + return WhoringBase.DataStore.GetBedData(bed).allowedForWhoringOwner; + } + + public static void SetAllowedForWhoringAll(this Building_Bed bed, bool isAllowed) + { + if (isAllowed) + { + // if whoring is allowed for all, also visibly allow for owner + bed.SetAllowedForWhoringOwner(true); + // if bed is designated for whoring, disable prisoner/medical use + bed.ForPrisoners = false; + bed.Medical = false; + } + WhoringBase.DataStore.GetBedData(bed).allowedForWhoringAll = isAllowed; + + bed.GetRoom()?.Notify_BedTypeChanged(); + bed.Notify_ColorChanged(); + } + public static void ToggleAllowedForWhoringAll(this Building_Bed bed) + { + bed.SetAllowedForWhoringAll(!WhoringBase.DataStore.GetBedData(bed).allowedForWhoringAll); + } + public static bool IsAllowedForWhoringAll(this Building_Bed bed) + { + if (WhoringBase.DataStore.GetBedData(bed).allowedForWhoringAll) + { + if (!bed.def.building.bed_humanlike || bed.Faction != Faction.OfPlayerSilentFail || bed.Medical || bed.ForPrisoners || bed.def.defName.Contains("Guest") || bed.GetRoom()?.IsPrisonCell == true) + { + return false; + } + return true; + } + return false; + } + + public static void ReserveForWhoring(this Building_Bed bed, Pawn p, int num_ticks) + { + WhoringBase.DataStore.GetBedData(bed).reservedUntilGameTick = GenTicks.TicksGame + num_ticks; + WhoringBase.DataStore.GetBedData(bed).reservedForPawnID = p.thingIDNumber; + } + public static void UnreserveForWhoring(this Building_Bed bed) + { + WhoringBase.DataStore.GetBedData(bed).reservedUntilGameTick = 0; + WhoringBase.DataStore.GetBedData(bed).reservedForPawnID = 0; + } + public static bool IsAvailableForWhoring(this Building_Bed bed, Pawn p) + { + // check for active reservation + if (WhoringBase.DataStore.GetBedData(bed).reservedUntilGameTick > GenTicks.TicksGame) + { + if (WhoringBase.DataStore.GetBedData(bed).reservedForPawnID != p.thingIDNumber) + { + // a different pawn has reserved this bed + return false; + } + } + + if (bed.OwnersForReading.Any()) + { + for (int i = 0; i < bed.OwnersForReading.Count; i++) + { + if (bed.OwnersForReading[i].InBed() && bed.OwnersForReading[i].CurrentBed() == bed) + { + // someone sleeping in this bed + return false; + } + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/Whoring_Helper.cs b/1.4/Source/Mod/Whoring_Helper.cs new file mode 100644 index 0000000..d313a98 --- /dev/null +++ b/1.4/Source/Mod/Whoring_Helper.cs @@ -0,0 +1,425 @@ +// #define TESTMODE // Uncomment to enable logging. + +using Verse; +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using System; +using UnityEngine; +using Verse.AI.Group; +//using Multiplayer.API; +using rjw; + +namespace rjwwhoring +{ + /// + /// Helper for whoring related stuff + /// + public class WhoringHelper + { + public const float baseMinPrice = 10f; + public const float baseMaxPrice = 20f; + + public static readonly HashSet backstories = new HashSet(DefDatabase.GetNamed("WhoreBackstories").strings); + + public static int WhoreMinPrice(Pawn whore) + { + float min_price = baseMinPrice; + min_price *= WhoreAgeAdjustment(whore); + min_price *= WhoreGenderAdjustment(whore); + min_price *= WhoreInjuryAdjustment(whore); + min_price *= WhoreAbilityAdjustmentMin(whore); + //min_price *= WhoreRoomAdjustment(whore); + if (xxx.has_traits(whore)) + min_price *= WhoreTraitAdjustmentMin(whore); + + return (int)min_price; + } + + public static int WhoreMaxPrice(Pawn whore) + { + float max_price = baseMaxPrice; + max_price *= WhoreAgeAdjustment(whore); + max_price *= WhoreGenderAdjustment(whore); + max_price *= WhoreInjuryAdjustment(whore); + max_price *= WhoreAbilityAdjustmentMax(whore); + //max_price *= WhoreRoomAdjustment(whore); + if (xxx.has_traits(whore)) + max_price *= WhoreTraitAdjustmentMax(whore); + + return (int)max_price; + } + + public static float WhoreGenderAdjustment(Pawn whore) + { + if (GenderHelper.GetSex(whore) == GenderHelper.Sex.futa) + return 1.2f; + return 1f; + } + + public static float WhoreAgeAdjustment(Pawn whore) + { + return AgeConfigDef.Instance.whoringPriceByAge.Evaluate(SexUtility.ScaleToHumanAge(whore)); + } + + 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 WhoreAbilityAdjustmentMin(Pawn whore) + { + int b = backstories.Contains(whore.story?.Adulthood?.titleShort) ? 30 : 0; + int score = whore.records.GetAsInt(RecordDefOf.EarnedMoneyByWhore); + float multiplier = (score + b) / 100; + multiplier = Math.Min(multiplier, 2); + multiplier = (multiplier - 0.5f) * 0.5f + 1; + return multiplier; + } + + public static float WhoreAbilityAdjustmentMax(Pawn whore) + { + int b = backstories.Contains(whore.story?.Adulthood?.titleShort) ? 30 : 0; + int score = whore.records.GetAsInt(RecordDefOf.CountOfWhore); + float multiplier = (score + b) / 100; + multiplier = Math.Min(multiplier, 2); + multiplier = (multiplier - 0.5f) * 0.5f + 1; + return multiplier; + } + + public static float WhoreTraitAdjustmentMin(Pawn whore) + { + float multiplier = WhoreTraitAdjustment(whore); + if (xxx.is_masochist(whore)) // Won't haggle, may settle for low price + multiplier *= 0.7f; + if (xxx.is_nympho(whore)) // Same as above + multiplier *= 0.4f; + if (whore.story.traits.HasTrait(TraitDefOf.Wimp)) // Same as above + multiplier *= 0.4f; + return multiplier; + } + + public static float WhoreTraitAdjustmentMax(Pawn whore) + { + float multiplier = WhoreTraitAdjustment(whore); + if (xxx.IndividualityIsActive && whore.story.traits.HasTrait(xxx.SYR_Haggler)) + multiplier *= 1.5f; + if (whore.story.traits.HasTrait(TraitDefOf.Greedy)) + multiplier *= 2f; + return multiplier; + } + + public static float WhoreTraitAdjustment(Pawn whore) + { + float multiplier = 1f; + + if (xxx.has_traits(whore)) + { + if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 2) // Industrious + multiplier *= 1.2f; + if (whore.story.traits.DegreeOfTrait(TraitDefOf.Industriousness) == 1) // Hard Worker + multiplier *= 1.1f; + if (whore.story.traits.HasTrait(TraitDefOf.CreepyBreathing)) + multiplier *= 0.75f; + + if (whore.GetStatValue(StatDefOf.PawnBeauty) >= 2) + multiplier *= 3.5f; + else if (whore.GetStatValue(StatDefOf.PawnBeauty) >= 1) + multiplier *= 2f; + else if (whore.GetStatValue(StatDefOf.PawnBeauty) < 0) + if (whore.GetStatValue(StatDefOf.PawnBeauty) >= -1) + multiplier *= 0.8f; + else + multiplier *= 0.6f; + } + return multiplier; + } + + /*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; + } + else if (scoreStageIndex > 3) + { + room_multiplier *= 1 + ((float)scoreStageIndex - 3) / 3.5f; + } //top room triples the price + + return room_multiplier; + }//*/ + + //[SyncMethod] + public static int PriceOfWhore(Pawn whore) + { + float NeedSexFactor = xxx.is_hornyorfrustrated(whore) ? 1 - xxx.need_some_sex(whore) / 8 : 1f; + + //Rand.PopState(); + //Rand.PushState(RJW_Multiplayer.PredictableSeed()); + float price = Rand.Range(WhoreMinPrice(whore), WhoreMaxPrice(whore)); + + price *= NeedSexFactor; + //--ModLog.Message(" 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; + if (WhoringBase.MoneyPrinting) return true; + + //if (RJWSettings.DebugWhoring) ModLog.Message($"CanAfford for client {xxx.get_pawnname(targetPawn)}"); + int price = priceOfWhore < 0 ? PriceOfWhore(whore) : priceOfWhore; + if (price == 0) + return true; + + // can customer afford the cheapest bed? - skip check, should rarely make a difference + //float bed_factor = WhoreBed_Utility.GetCheapestBedFactor(whore, targetPawn); + //price = (int)(price * bed_factor); + + //if (RJWSettings.DebugWhoring) ModLog.Message($" whore price {price}"); + + Lord lord = targetPawn.GetLord(); + Faction faction = targetPawn.Faction; + int clientAmountOfSilvers = targetPawn.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver); + int totalAmountOfSilvers = clientAmountOfSilvers; + + if (faction != null) + { + List caravanMembers = targetPawn.Map.mapPawns.PawnsInFaction(targetPawn.Faction).Where(x => x.GetLord() == lord && x.inventory?.innerContainer?.TotalStackCountOfDef(ThingDefOf.Silver) > 0).ToList(); + if (!caravanMembers.Any()) + { + //if (RJWSettings.DebugWhoring) ModLog.Message($" client not in caravan"); + //if (RJWSettings.DebugWhoring) ModLog.Message("CanAfford::(" + xxx.get_pawnname(targetPawn) + "," + xxx.get_pawnname(whore) + ") - totalAmountOfSilvers is " + totalAmountOfSilvers); + return totalAmountOfSilvers >= price; + } + + totalAmountOfSilvers += caravanMembers.Sum(member => member.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver)); + } + //if (RJWSettings.DebugWhoring) ModLog.Message($" client silver = {clientAmountOfSilvers} caravan silver = {totalAmountOfSilvers - clientAmountOfSilvers}"); + + //if (RJWSettings.DebugWhoring) ModLog.Message("CanAfford:: can afford the whore: " + (totalAmountOfSilvers >= 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) + { + if (WhoringBase.DebugWhoring) ModLog.Message($"PayPriceToWhore for client {xxx.get_pawnname(targetPawn)}"); + if (WhoringBase.MoneyPrinting) + { + DebugThingPlaceHelper.DebugSpawn(ThingDefOf.Silver, whore.PositionHeld, priceOfWhore, false, null); + if (WhoringBase.DebugWhoring) ModLog.Message($" MoneyPrinting " + priceOfWhore + " silver to pay price"); + return 0; + } + int AmountLeft = priceOfWhore; + if ((targetPawn.Faction == whore.Faction && targetPawn.GuestStatus != GuestStatus.Guest) || priceOfWhore == 0) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" 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 caravan = targetPawn.Map.mapPawns.PawnsInFaction(targetPawn.Faction).Where(x => x.GetLord() == lord && x.inventory?.innerContainer != null && x.inventory.innerContainer.TotalStackCountOfDef(ThingDefOf.Silver) > 0).ToList(); + + IEnumerable TraderSilvers; + if (!caravan.Any()) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" (not a caravan member), try to pay with own silver"); + TraderSilvers = targetPawn.inventory.innerContainer.Where(x => x.def == ThingDefOf.Silver); + foreach (Thing silver in TraderSilvers) + { + if (AmountLeft <= 0) + break; + 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) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" have no silver"); + continue; + } + AmountLeft -= resultingSilvers.stackCount; + if (AmountLeft <= 0) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(targetPawn)} paid {resultingSilvers.stackCount} silver"); + break; + } + } + else + { + if (WhoringBase.DebugWhoring) ModLog.Message($" TryDrop failed"); + break; + } + } + if (WhoringBase.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore - AmountLeft}"); + } + else + { + if (WhoringBase.DebugWhoring) ModLog.Message($" (caravan member), try to pay {AmountLeft} silver with caravan silver"); + foreach (Pawn caravanMember in caravan) + { + TraderSilvers = caravanMember.inventory.innerContainer.Where(x => x.def == ThingDefOf.Silver); + if (WhoringBase.DebugWhoring) ModLog.Message($" try to pay with {xxx.get_pawnname(caravanMember)} silver"); + foreach (Thing silver in TraderSilvers) + { + if (AmountLeft <= 0) + break; + int dropAmount = silver.stackCount >= AmountLeft ? AmountLeft : silver.stackCount; + if (caravanMember.inventory.innerContainer.TryDrop(silver, whore.Position, whore.Map, ThingPlaceMode.Near, dropAmount, out Thing resultingSilvers)) + { + if (resultingSilvers is null) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" have no silver"); + continue; + } + AmountLeft -= resultingSilvers.stackCount; + if (AmountLeft <= 0) + { + if (WhoringBase.DebugWhoring) ModLog.Message($" {xxx.get_pawnname(caravanMember)} paid {resultingSilvers.stackCount} silver"); + break; + } + } + } + } + } + if (WhoringBase.DebugWhoring) ModLog.Message($" price: {priceOfWhore}, paid: {priceOfWhore - AmountLeft}"); + return AmountLeft; + } + + //[SyncMethod] + public static bool IsHookupAppealing(Pawn target, Pawn whore) + { + if (PawnUtility.WillSoonHaveBasicNeed(target)) + { + //Log.Message("IsHookupAppealing - fail: " + xxx.get_pawnname(target) + " has need to do"); + return false; + } + if (WhoringBase.ClientAlwaysAccept) + { + return true; + } + float num = target.relations.SecondaryRomanceChanceFactor(whore) / 1.5f; + if (xxx.is_frustrated(target)) + { + num *= 3.0f; + } + else if (xxx.is_hornyorfrustrated(target)) + { + 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)); // 1 to 0 reduce score by negative opinion/relations to whore + //Log.Message("IsHookupAppealing - score: " + num); + //Rand.PopState(); + //Rand.PushState(RJW_Multiplayer.PredictableSeed()); + return Rand.Range(0.05f, 1f) < num; + } + + /// + /// Check if the pawn is willing to hook up. Checked for both target and the whore(when hooking colonists). + /// + //[SyncMethod] + public static bool WillPawnTryHookup(Pawn target) + { + if (WhoringBase.ClientAlwaysAccept) + { + return true; + } + if (target.story.traits.HasTrait(TraitDefOf.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.is_frustrated(target)) + { + num2 *= 1.8f; + } + else if (xxx.is_hornyorfrustrated(target)) + { + num2 *= 1.4f; + } + num2 /= 1.5f; + //Rand.PopState(); + //Rand.PushState(RJW_Multiplayer.PredictableSeed()); + return Rand.Range(0f, 1f) < num2; + } + + /// + /// Updates records for whoring. + /// + public static void UpdateRecords(Pawn pawn, int price) + { + pawn.records.AddTo(RecordDefOf.EarnedMoneyByWhore, price); + pawn.records.Increment(RecordDefOf.CountOfWhore); + //this is added by normal outcome + //pawn.records.Increment(CountOfSex); + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/harmony_AftersexPatch.cs b/1.4/Source/Mod/harmony_AftersexPatch.cs new file mode 100644 index 0000000..e7accc6 --- /dev/null +++ b/1.4/Source/Mod/harmony_AftersexPatch.cs @@ -0,0 +1,64 @@ +using Verse; +using HarmonyLib; +using rjw; +using System; +using RimWorld; + +namespace rjwwhoring +{ + [HarmonyPatch(typeof(AfterSexUtility), "think_about_sex", new Type[] {typeof(Pawn), typeof(Pawn), typeof(bool), typeof(SexProps), typeof(bool)})] + [StaticConstructorOnStartup] + static class Aftersex_WhoringhoughtApply + { + [HarmonyPostfix] + private static void ThinkAboutWhoringPatch(Pawn pawn, Pawn partner, bool isReceiving, SexProps props, bool whoring = false) + { + try + { + AfterSexUtilityPatch.ThinkAboutWhoring(pawn, partner, isReceiving, props, whoring); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + } + } + + static class AfterSexUtilityPatch + { + /// + ///add aftersex thoughts for whoring + /// + public static void ThinkAboutWhoring(Pawn pawn, Pawn partner, bool isReceiving, SexProps props, bool whoring = false) + { + //no whoring in vanilla sex + if (props.isCoreLovin) + return; + + //masturbation? + if (pawn == null || partner == null) + return; + + //necro + if (pawn.Dead || partner.Dead) + return; + + //bestiality + if (!(xxx.is_human(pawn) && xxx.is_human(partner))) + return; + + //whoring, initiator is whore + if (whoring && !props.isReceiver) + { + ThoughtDef memory; + + if (pawn.IsPrisoner || xxx.is_slave(pawn)) + memory = ThoughtDefOf.Whorish_Thoughts_Captive; + else + memory = ThoughtDefOf.Whorish_Thoughts; + + pawn.needs.mood.thoughts.memories.TryGainMemory(memory); + } + } + } +} diff --git a/1.4/Source/Mod/harmony_Building_BedPatches.cs b/1.4/Source/Mod/harmony_Building_BedPatches.cs new file mode 100644 index 0000000..852b633 --- /dev/null +++ b/1.4/Source/Mod/harmony_Building_BedPatches.cs @@ -0,0 +1,292 @@ +using HarmonyLib; +using Verse; +using System; +using RimWorld; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using rjw; + +/// +/// patches Building_Bed to add stuff for WhoreBeds +/// +/// Also contains smaller patches for RoomRoleWorker_Barracks (don't count whore beds) (disabled) and Toils_LayDown.ApplyBedThoughts (slept in brothel thought) +/// + +namespace rjwwhoring +{ + public static class harmony_Building_BedPatches + { + + private static readonly Color sheetColorForWhores = new Color(181 / 255f, 55 / 255f, 109 / 255f); + + // Set color for whore beds + [HarmonyPatch(typeof(Building_Bed))] + [HarmonyPatch(nameof(Building_Bed.DrawColorTwo), MethodType.Getter)] + public static class Building_Bed_DrawColor_Patch + { + [HarmonyPostfix] + public static void Postfix(Building_Bed __instance, ref Color __result) + { + if (__instance.IsAllowedForWhoringAll()) + { + __result = sheetColorForWhores; + } + } + } + + // add whoring toggles to beds + [HarmonyPatch(typeof(Building_Bed), nameof(Building_Bed.GetGizmos))] + public static class Building_Bed_GetGizmos_Patch + { + [HarmonyPostfix] + public static void Postfix(Building_Bed __instance, ref IEnumerable __result) + { + if (!WhoringBase.show_whore_widgets_on_bed) + { + return; + } + __result = Process(__instance, __result); + } + + private static IEnumerable Process(Building_Bed __instance, IEnumerable __result) + { + var isPrisonCell = __instance.GetRoom()?.IsPrisonCell == true; + if (!__instance.ForPrisoners && !__instance.Medical && __instance.def.building.bed_humanlike && __instance.Faction == Faction.OfPlayerSilentFail && !__instance.def.defName.Contains("Guest") && !isPrisonCell) + { + + yield return + new Command_Toggle + { + defaultLabel = "CommandBedAllowWhoringLabel".Translate(), + defaultDesc = "CommandBedAllowWhoringDesc".Translate(), + icon = ContentFinder.Get("UI/Commands/AsWhore"), + isActive = __instance.IsAllowedForWhoringOwner, + toggleAction = __instance.ToggleAllowedForWhoringOwner, + hotKey = KeyBindingDefOf.Misc5, // Guest Beds uses Misc4 + disabled = !__instance.def.HasAssignableCompFrom(typeof(CompAssignableToPawn_Bed)), + disabledReason = "This bed type is not assignable to pawns." + }; + + yield return + new Command_Toggle + { + defaultLabel = "CommandBedSetAsWhoreBedLabel".Translate(), + defaultDesc = "CommandBedSetAsWhoreBedDesc".Translate(), + icon = ContentFinder.Get("UI/Commands/AsWhoreMany"), + isActive = __instance.IsAllowedForWhoringAll, + toggleAction = __instance.ToggleAllowedForWhoringAll, + hotKey = KeyBindingDefOf.Misc6, // Guest Beds uses Misc4 + disabled = !__instance.def.HasAssignableCompFrom(typeof(CompAssignableToPawn_Bed)), + disabledReason = "This bed type is not assignable to pawns." + }; + } + + foreach (var gizmo in __result) + { + if (__instance.IsAllowedForWhoringAll()) + { + if (gizmo is Command_Toggle && ((Command_Toggle)gizmo).defaultLabel == "CommandBedSetAsGuestLabel".Translate()) + { + // hide set as guest bed + continue; + }; + // old: instead of hiding, just disable + /*switch (gizmo) + { + case Command_Toggle toggle: + { + // Disable prisoner and medical, and guest buttons + if (//toggle.defaultLabel == "CommandBedSetForPrisonersLabel".Translate() || + //toggle.defaultLabel == "CommandBedSetAsMedicalLabel".Translate() || + toggle.defaultLabel == "CommandBedSetAsGuestLabel".Translate()) gizmo.Disable(); + break; + } + }//*/ + } + yield return gizmo; + } + } + } + + // add description of whore price factor to inspect string (bottom left corner if item selected) + [HarmonyPatch(typeof(Building_Bed), nameof(Building_Bed.GetInspectString))] + public static class Building_Bed_GetInspectString_Patch + { + [HarmonyPostfix] + public static void Postfix(Building_Bed __instance, ref string __result) + { + if (__instance.def.building.bed_humanlike && __instance.Faction == Faction.OfPlayerSilentFail && (__instance.IsAllowedForWhoringAll() || __instance.IsAllowedForWhoringOwner())) + { + __result = __result + "\n" + "WhorePriceCalcDesc".Translate(WhoreBed_Utility.CalculatePriceFactor(__instance).ToString("F2")); + if (WhoringBase.DebugWhoring) + { + __result = __result + "\nbed.thingIDNumber: " + __instance.thingIDNumber.ToString(); + + __result = __result + "\nscoreUpdateTickDelay: " + WhoringBase.DataStore.GetBedData(__instance).scoreUpdateTickDelay.ToString(); + + if (WhoringBase.DataStore.GetBedData(__instance).reservedUntilGameTick > GenTicks.TicksGame) + { + __result = __result + "\nreserved by pawn id: " + WhoringBase.DataStore.GetBedData(__instance).reservedForPawnID.ToString(); + } + } + } + } + } + + // add whore price factor as overlay + [HarmonyPatch(typeof(Building_Bed), nameof(Building_Bed.DrawGUIOverlay))] + public static class Building_Bed_DrawGUIOverlay_Patch + { + [HarmonyPrefix] + public static bool Prefix(Building_Bed __instance) + { + if (WhoringBase.show_whore_price_factor_on_bed && __instance.def.building.bed_humanlike && __instance.Faction == Faction.OfPlayerSilentFail) { + // if whore bed, print price factor on it + if (Find.CameraDriver.CurrentZoom == CameraZoomRange.Closest + && ((__instance.IsAllowedForWhoringOwner() && __instance.OwnersForReading.Any()) + || __instance.IsAllowedForWhoringAll())) + { + Color defaultThingLabelColor = GenMapUI.DefaultThingLabelColor; + + // make string + float whore_price_factor = WhoreBed_Utility.CalculatePriceFactor(__instance); + string wpf; + if (Math.Abs(whore_price_factor) >= 100) + { + wpf = ((int)whore_price_factor).ToString("D"); + } + else if (Math.Abs(whore_price_factor) >= 10) + { + wpf = whore_price_factor.ToString("F1"); + } + else + { + wpf = whore_price_factor.ToString("F2"); + } + + // get dimensions of text and make it appear above names + Vector2 textsize = Text.CalcSize(wpf); + Vector2 baseLabelPos = GenMapUI.LabelDrawPosFor(__instance, -0.4f); // -0.4f is copied from vanilla code + baseLabelPos.y -= textsize.y * 0.75f; + + GenMapUI.DrawThingLabel(baseLabelPos, wpf, defaultThingLabelColor); + + if (__instance.IsAllowedForWhoringAll() && !__instance.OwnersForReading.Any()) + { + // hide "Unowned" if whore bed with no owner + return false; + } + } + } + // after drawing whore price factor, draw the actual names + // could have been done as a postfix, but I started with a prefix, hoping I could get by with only one draw call + return true; + } + + } + + // barracks don't count whore beds, so room type switches to brothel sooner + // disabled - barracks have their own slept in ~ debuff; doesn't really matter; put some effort in your brothels! + /*[HarmonyPatch(typeof(RoomRoleWorker_Barracks), nameof(RoomRoleWorker_Barracks.GetScore))] + public static class RoomRoleWorker_Barracks_GetScore_Patch + { + public static bool Prefix(Room room, ref float __result) + { + int num = 0; + int num2 = 0; + List containedAndAdjacentThings = room.ContainedAndAdjacentThings; + for (int i = 0; i < containedAndAdjacentThings.Count; i++) + { + Building_Bed building_Bed = containedAndAdjacentThings[i] as Building_Bed; + if (building_Bed != null && building_Bed.def.building.bed_humanlike) + { + if (building_Bed.ForPrisoners) + { + __result = 0f; + return false; + } + num++; + if (!building_Bed.Medical && !building_Bed.IsAllowedForWhoringAll()) + { + num2++; + } + } + } + if (num <= 1) + { + __result = 0f; + return false; + } + __result = (float)num2 * 100100f; + return false; + } + }*/ + + // if pawns sleep in a brothel or a whoring bed, they get a thought + [HarmonyPatch(typeof(Toils_LayDown), "ApplyBedThoughts")] + public class Toils_LayDown_ApplyBedThoughts_Patch + { + [HarmonyPostfix] + public static void Postfix(Pawn actor) + { + if (actor?.needs?.mood == null) return; + + Building_Bed building_Bed = actor.CurrentBed(); + + actor?.needs?.mood?.thoughts?.memories?.RemoveMemoriesOfDef(ThoughtDefOf.SleptInBrothel); + + if (building_Bed == null) return; + + if (building_Bed?.GetRoom()?.Role == WhoreBed_Utility.roleDefBrothel || building_Bed.IsAllowedForWhoringAll()) + { + var memoryHandler = actor.needs.mood.thoughts.memories; + int thoughtStage = 0; + + foreach (var thoughtDef in DefDatabase.AllDefsListForReading) + { + var memory = memoryHandler.GetFirstMemoryOfDef(thoughtDef); + if (memory?.CurStageIndex >= thoughtDef.stages.Count - 1) + { + thoughtStage = 1; + break; + } + } + + memoryHandler.TryGainMemory(ThoughtMaker.MakeThought(ThoughtDefOf.SleptInBrothel, thoughtStage)); + } + } + } + + // if room stats are updated, update beds within + // "necessary" if beds are toggled during pause + [HarmonyPatch(typeof(Room), "UpdateRoomStatsAndRole")] + public class Room_UpdateRoomStatsAndRole_Patch + { + [HarmonyPostfix] + public static void Postfix(Room __instance) + { + // note: with room stat display enabled, this get's called quite often for whatever room the mouse hovers over + // even large outdoor areas + // where iterating over all things to find all beds (even if there are none) is expensive + // for now, skip doing anything if even the game decides it's not worth it + // (game sets role to None if region count >36 or something) + // - the beds will update eventually + + if (/*Find.PlaySettings.showRoomStats && */__instance.Role == RoomRoleDefOf.None) + return; + + if (Find.TickManager.Paused) + { + // if paused, update immediately + WhoreBed_Utility.CalculateBedFactorsForRoom(__instance); + } + else + { + // else, just make beds update as soon as needed + WhoreBed_Utility.ResetTicksUntilUpdate(__instance); + } + } + } + } +} \ No newline at end of file diff --git a/1.4/Source/Mod/harmony_RJWTab_patch.cs b/1.4/Source/Mod/harmony_RJWTab_patch.cs new file mode 100644 index 0000000..5f32cd0 --- /dev/null +++ b/1.4/Source/Mod/harmony_RJWTab_patch.cs @@ -0,0 +1,53 @@ +using Verse; +using HarmonyLib; +using rjw; +using System; +using RimWorld; +using System.Collections.Generic; +using UnityEngine; +using rjw.MainTab.DefModExtensions; +using System.Linq; +using System.Reflection; + +namespace rjwwhoring +{ + [HarmonyPatch(typeof(rjw.MainTab.MainTabWindow))] + [HarmonyPatch(nameof(rjw.MainTab.MainTabWindow.MakeOptions))] + static class RJWTab_Brothel + { + [HarmonyPostfix] + private static void MakeOptionsPatch(rjw.MainTab.MainTabWindow __instance, ref List __result) + { + try + { + RJWTab_Brothel_Patch.MakeOptionsPatch(__instance, ref __result); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + } + } + + static class RJWTab_Brothel_Patch + { + public static List MakeOptionsPatch(rjw.MainTab.MainTabWindow __instance, ref List __result) + { + PawnTableDef RJW_Brothel = DefDatabase.GetNamed("RJW_Brothel"); + ModLog.Message("0"); + __result.Add(new FloatMenuOption(RJW_Brothel.GetModExtension().label, () => + { + ModLog.Message("1"); + __instance.pawnTableDef = RJW_Brothel; + ModLog.Message("2"); + __instance.pawns = Find.CurrentMap.mapPawns.AllPawns.Where(p => xxx.is_human(p) && (p.IsColonist || p.IsPrisonerOfColony)); + ModLog.Message("3"); + __instance.Notify_ResolutionChanged(); + ModLog.Message("4"); + rjw.MainTab.MainTabWindow.Reloadtab(); + ModLog.Message("5"); + }, MenuOptionPriority.Default)); + return __result; + } + } +} diff --git a/1.4/Source/Mod/packages.config b/1.4/Source/Mod/packages.config new file mode 100644 index 0000000..c5bef78 --- /dev/null +++ b/1.4/Source/Mod/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/1.4/Source/Properties/AssemblyInfo.cs b/1.4/Source/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7b4716d --- /dev/null +++ b/1.4/Source/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RimJobWorld Whoring")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RimJobWorld Whoring")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c2825019-7f0b-456d-85a3-479c1a2a8805")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/1.4/Source/mod.sln b/1.4/Source/mod.sln new file mode 100644 index 0000000..981a536 --- /dev/null +++ b/1.4/Source/mod.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Whoring", "Mod\Whoring.csproj", "{3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FC2D442-19B8-4CF9-9D35-CD13B6AC7B28}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5A0C2732-36A9-4ACA-807E-019E02C37E10} + EndGlobalSection +EndGlobal diff --git a/1.4/Textures/UI/Commands/AsWhore.png b/1.4/Textures/UI/Commands/AsWhore.png new file mode 100644 index 0000000..b5f14e1 Binary files /dev/null and b/1.4/Textures/UI/Commands/AsWhore.png differ diff --git a/1.4/Textures/UI/Commands/AsWhoreMany.png b/1.4/Textures/UI/Commands/AsWhoreMany.png new file mode 100644 index 0000000..f7c1989 Binary files /dev/null and b/1.4/Textures/UI/Commands/AsWhoreMany.png differ diff --git a/1.4/Textures/UI/Commands/Service_Refuse.png b/1.4/Textures/UI/Commands/Service_Refuse.png new file mode 100644 index 0000000..7b57c3b Binary files /dev/null and b/1.4/Textures/UI/Commands/Service_Refuse.png differ diff --git a/1.4/Textures/UI/Commands/Service_off.png b/1.4/Textures/UI/Commands/Service_off.png new file mode 100644 index 0000000..3b46e97 Binary files /dev/null and b/1.4/Textures/UI/Commands/Service_off.png differ diff --git a/1.4/Textures/UI/Commands/Service_on.png b/1.4/Textures/UI/Commands/Service_on.png new file mode 100644 index 0000000..b427f88 Binary files /dev/null and b/1.4/Textures/UI/Commands/Service_on.png differ diff --git a/1.4/Textures/UI/Tab/ComfortPrisoner_off.png b/1.4/Textures/UI/Tab/ComfortPrisoner_off.png new file mode 100644 index 0000000..7cb8f64 Binary files /dev/null and b/1.4/Textures/UI/Tab/ComfortPrisoner_off.png differ diff --git a/1.4/Textures/UI/Tab/ComfortPrisoner_off_nobg.png b/1.4/Textures/UI/Tab/ComfortPrisoner_off_nobg.png new file mode 100644 index 0000000..ce033a1 Binary files /dev/null and b/1.4/Textures/UI/Tab/ComfortPrisoner_off_nobg.png differ diff --git a/1.4/Textures/UI/Tab/ComfortPrisoner_on.png b/1.4/Textures/UI/Tab/ComfortPrisoner_on.png new file mode 100644 index 0000000..c2a76ee Binary files /dev/null and b/1.4/Textures/UI/Tab/ComfortPrisoner_on.png differ diff --git a/1.4/Textures/UI/Tab/Service_off.png b/1.4/Textures/UI/Tab/Service_off.png new file mode 100644 index 0000000..510041b Binary files /dev/null and b/1.4/Textures/UI/Tab/Service_off.png differ diff --git a/1.4/Textures/UI/Tab/Service_on.png b/1.4/Textures/UI/Tab/Service_on.png new file mode 100644 index 0000000..cdcc073 Binary files /dev/null and b/1.4/Textures/UI/Tab/Service_on.png differ diff --git a/About/About.xml b/About/About.xml index 4bea346..f068906 100644 --- a/About/About.xml +++ b/About/About.xml @@ -6,6 +6,7 @@ https://gitgud.io/Ed86/rjw-whoring
  • 1.3
  • +
  • 1.4
  • rjw.whoring