diff --git a/Content.Server/FloofStation/Traits/Components/CumProducerComponent.cs b/Content.Server/FloofStation/Traits/Components/CumProducerComponent.cs new file mode 100644 index 00000000000..aa7ed3fc009 --- /dev/null +++ b/Content.Server/FloofStation/Traits/Components/CumProducerComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FloofStation.Traits; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.GameStates; + +namespace Content.Server.FloofStation.Traits; + +[RegisterComponent, Access(typeof(LewdTraitSystem))] +public sealed partial class CumProducerComponent : Component +{ + [DataField("solutionname"), ViewVariables(VVAccess.ReadWrite)] + public string SolutionName; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public ProtoId ReagentId = "Cum"; + + [DataField] + public FixedPoint2 MaxVolume = FixedPoint2.New(25); + + [DataField] + public Entity? Solution = null; + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public FixedPoint2 QuantityPerUpdate = 25; + + [DataField] + public float HungerUsage = 10f; + + [DataField] + public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1); + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextGrowth = TimeSpan.FromSeconds(0); +} diff --git a/Content.Server/FloofStation/Traits/Components/MilkProducerComponent.cs b/Content.Server/FloofStation/Traits/Components/MilkProducerComponent.cs new file mode 100644 index 00000000000..f557394c94c --- /dev/null +++ b/Content.Server/FloofStation/Traits/Components/MilkProducerComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FloofStation.Traits; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.GameStates; + +namespace Content.Server.FloofStation.Traits; + +[RegisterComponent, Access(typeof(LewdTraitSystem))] +public sealed partial class MilkProducerComponent : Component +{ + [DataField("solutionname"), ViewVariables(VVAccess.ReadWrite)] + public string SolutionName; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public ProtoId ReagentId = "Milk"; + + [DataField] + public FixedPoint2 MaxVolume = FixedPoint2.New(25); + + [DataField] + public Entity? Solution = null; + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public FixedPoint2 QuantityPerUpdate = 25; + + [DataField] + public float HungerUsage = 10f; + + [DataField] + public TimeSpan GrowthDelay = TimeSpan.FromMinutes(1); + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextGrowth = TimeSpan.FromSeconds(0); +} diff --git a/Content.Server/FloofStation/Traits/LewdTraitSystem.cs b/Content.Server/FloofStation/Traits/LewdTraitSystem.cs new file mode 100644 index 00000000000..1429488a47a --- /dev/null +++ b/Content.Server/FloofStation/Traits/LewdTraitSystem.cs @@ -0,0 +1,328 @@ +using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.Popups; +using Content.Shared.Chemistry.Components; +using Content.Shared.DoAfter; +using Content.Shared.IdentityManagement; +using Content.Shared.Mobs.Systems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Content.Shared.FloofStation.Traits.Events; +using Robust.Shared.Timing; +using JetBrains.Annotations; + +namespace Content.Server.FloofStation.Traits; + +[UsedImplicitly] +public sealed class LewdTraitSystem : EntitySystem +{ + [Dependency] private readonly HungerSystem _hunger = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + + public override void Initialize() + { + base.Initialize(); + + //Initializers + SubscribeLocalEvent(OnComponentInitCum); + SubscribeLocalEvent(OnComponentInitMilk); + //SubscribeLocalEvent(OnComponentInitSquirt); //Unused-Trait is WIP + + //Verbs + SubscribeLocalEvent>(AddCumVerb); + SubscribeLocalEvent>(AddMilkVerb); + //SubscribeLocalEvent>(AddSquirtVerb); //Unused-Trait is WIP + + //Events + SubscribeLocalEvent(OnDoAfterCum); + SubscribeLocalEvent(OnDoAfterMilk); + //SubscribeLocalEvent(OnDoAfterSquirt); //Unused-Trait is WIP + } + + #region event handling + private void OnComponentInitCum(Entity entity, ref ComponentStartup args) + { + var solutionCum = _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + solutionCum.MaxVolume = entity.Comp.MaxVolume; + + solutionCum.AddReagent(entity.Comp.ReagentId, entity.Comp.MaxVolume - solutionCum.Volume); + } + + private void OnComponentInitMilk(Entity entity, ref ComponentStartup args) + { + var solutionMilk = _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + solutionMilk.MaxVolume = entity.Comp.MaxVolume; + + solutionMilk.AddReagent(entity.Comp.ReagentId, entity.Comp.MaxVolume - solutionMilk.Volume); + } + + //private void OnComponentInitSquirt(Entity entity, ref ComponentStartup args) //Unused-Trait is WIP + //{ + // var solutionSquirt = _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + // solutionSquirt.MaxVolume = entity.Comp.MaxVolume; + + // solutionSquirt.AddReagent(entity.Comp.ReagentId, entity.Comp.MaxVolume - solutionSquirt.Volume); + //} + + public void AddCumVerb(Entity entity, ref GetVerbsEvent args) + { + if (args.Using == null || + !args.CanInteract || + args.User != args.Target || + !EntityManager.HasComponent(args.Using.Value)) //see if removing this part lets you milk on the ground. + return; + + _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + + var user = args.User; + var used = args.Using.Value; + + InnateVerb verbCum = new() + { + Act = () => AttemptCum(entity, user, used), + Text = Loc.GetString($"cum-verb-get-text"), + Priority = 1 + }; + args.Verbs.Add(verbCum); + } + + public void AddMilkVerb(Entity entity, ref GetVerbsEvent args) + { + if (args.Using == null || + !args.CanInteract || + args.User != args.Target || + !EntityManager.HasComponent(args.Using.Value)) //see if removing this part lets you milk on the ground. + return; + + _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + + var user = args.User; + var used = args.Using.Value; + + InnateVerb verbMilk = new() + { + Act = () => AttemptMilk(entity, user, used), + Text = Loc.GetString($"milk-verb-get-text"), + Priority = 1 + }; + args.Verbs.Add(verbMilk); + } + + //public void AddSquirtVerb(Entity entity, ref GetVerbsEvent args) //Unused-Trait is WIP + //{ + // if (args.Using == null || + // !args.CanInteract || + // !EntityManager.HasComponent(args.Using.Value)) //see if removing this part lets you milk on the ground. + // return; + + // _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + + // var user = args.User; + // var used = args.Using.Value; + + // InnateVerb verbSquirt = new() + // { + // Act = () => AttemptSquirt(entity, user, used), + // Text = Loc.GetString($"squirt-verb-get-text"), + // Priority = 1 + // }; + // args.Verbs.Add(verbSquirt); + //} + + private void OnDoAfterCum(Entity entity, ref CummingDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Used == null) + return; + + if (!_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution)) + return; + + if (!_solutionContainer.TryGetRefillableSolution(args.Args.Used.Value, out var targetSoln, out var targetSolution)) + return; + + args.Handled = true; + var quantity = solution.Volume; + if (quantity == 0) + { + _popupSystem.PopupEntity(Loc.GetString("cum-verb-dry"), entity.Owner, args.Args.User); + return; + } + + if (quantity > targetSolution.AvailableVolume) + quantity = targetSolution.AvailableVolume; + + var split = _solutionContainer.SplitSolution(entity.Comp.Solution.Value, quantity); + _solutionContainer.TryAddSolution(targetSoln.Value, split); + _popupSystem.PopupEntity(Loc.GetString("cum-verb-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner, args.Args.User, PopupType.Medium); + } + + private void OnDoAfterMilk(Entity entity, ref MilkingDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Used == null) + return; + + if (!_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution)) + return; + + if (!_solutionContainer.TryGetRefillableSolution(args.Args.Used.Value, out var targetSoln, out var targetSolution)) + return; + + args.Handled = true; + var quantity = solution.Volume; + if (quantity == 0) + { + _popupSystem.PopupEntity(Loc.GetString("milk-verb-dry"), entity.Owner, args.Args.User); + return; + } + + if (quantity > targetSolution.AvailableVolume) + quantity = targetSolution.AvailableVolume; + + var split = _solutionContainer.SplitSolution(entity.Comp.Solution.Value, quantity); + _solutionContainer.TryAddSolution(targetSoln.Value, split); + _popupSystem.PopupEntity(Loc.GetString("milk-verb-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner, args.Args.User, PopupType.Medium); + } + + //private void OnDoAfterSquirt(Entity entity, ref SquirtingDoAfterEvent args) //Unused-Trait is WIP + //{ + // if (args.Cancelled || args.Handled || args.Args.Used == null) + // return; + + // if (!_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution)) + // return; + + // if (!_solutionContainer.TryGetRefillableSolution(args.Args.Used.Value, out var targetSoln, out var targetSolution)) + // return; + + // args.Handled = true; + // var quantity = solution.Volume; + // if (quantity == 0) + // { + // _popupSystem.PopupEntity(Loc.GetString("squirt-verb-dry"), entity.Owner, args.Args.User); + // return; + // } + + // if (quantity > targetSolution.AvailableVolume) + // quantity = targetSolution.AvailableVolume; + + // var split = _solutionContainer.SplitSolution(entity.Comp.Solution.Value, quantity); + // _solutionContainer.TryAddSolution(targetSoln.Value, split); + // _popupSystem.PopupEntity(Loc.GetString("squirt-verb-success", ("amount", quantity), ("target", Identity.Entity(args.Args.Used.Value, EntityManager))), entity.Owner, args.Args.User, PopupType.Medium); + //} + #endregion + + #region utilities + private void AttemptCum(Entity lewd, EntityUid userUid, EntityUid containerUid) + { + if (!HasComp(userUid)) + return; + + var doargs = new DoAfterArgs(EntityManager, userUid, 5, new CummingDoAfterEvent(), lewd, lewd, used: containerUid) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + MovementThreshold = 1.0f, + }; + + _doAfterSystem.TryStartDoAfter(doargs); + } + + private void AttemptMilk(Entity lewd, EntityUid userUid, EntityUid containerUid) + { + if (!HasComp(userUid)) + return; + + var doargs = new DoAfterArgs(EntityManager, userUid, 5, new MilkingDoAfterEvent(), lewd, lewd, used: containerUid) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + MovementThreshold = 1.0f, + }; + + _doAfterSystem.TryStartDoAfter(doargs); + } + + //private void AttemptSquirt(Entity lewd, EntityUid userUid, EntityUid containerUid) //Unused-Trait is WIP + //{ + // if (!HasComp(userUid)) + // return; + + // var doargs = new DoAfterArgs(EntityManager, userUid, 5, new SquirtingDoAfterEvent(), lewd, lewd, used: containerUid) + // { + // BreakOnUserMove = true, + // BreakOnDamage = true, + // BreakOnTargetMove = true, + // MovementThreshold = 1.0f, + // }; + + // _doAfterSystem.TryStartDoAfter(doargs); + //} + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = AllEntityQuery(); //SquirtProducerComponent -unused + + while (query.MoveNext(out var uid, out var containerCum, out var containerMilk)) // out var containerSquirt -unused + { + if (_mobState.IsDead(uid)) + continue; + + if (!(now < containerCum.NextGrowth)) + { + containerCum.NextGrowth = now + containerCum.GrowthDelay; + + // Actually there is food digestion so no problem with instant reagent generation "OnFeed" + if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) + { + // Is there enough nutrition to produce reagent? + if (!(_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)) + _hunger.ModifyHunger(uid, -containerCum.HungerUsage, hunger); + } + + if (_solutionContainer.ResolveSolution(uid, containerCum.SolutionName, ref containerCum.Solution)) + _solutionContainer.TryAddReagent(containerCum.Solution.Value, containerCum.ReagentId, containerCum.QuantityPerUpdate, out _); + } + + if (!(now < containerMilk.NextGrowth)) + { + containerMilk.NextGrowth = now + containerMilk.GrowthDelay; + + if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) + { + if (!(_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)) + _hunger.ModifyHunger(uid, -containerMilk.HungerUsage, hunger); + } + + if (_solutionContainer.ResolveSolution(uid, containerMilk.SolutionName, ref containerMilk.Solution)) + _solutionContainer.TryAddReagent(containerMilk.Solution.Value, containerMilk.ReagentId, containerMilk.QuantityPerUpdate, out _); + } + + //if (!(now < containerSquirt.NextGrowth)) //Unused-Trait is WIP + //{ + // containerSquirt.NextGrowth = now + containerSquirt.GrowthDelay; + + // + // if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) + // { + // + // if (!(_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay)) + // _hunger.ModifyHunger(uid, -containerSquirt.HungerUsage, hunger); + // } + + // if (_solutionContainer.ResolveSolution(uid, containerSquirt.SolutionName, ref containerSquirt.Solution)) + // _solutionContainer.TryAddReagent(containerSquirt.Solution.Value, containerSquirt.ReagentId, containerSquirt.QuantityPerUpdate, out _); + //} + } + } + #endregion +} diff --git a/Content.Shared/Floofstation/Traits/Events/CummingDoAfterEvent.cs b/Content.Shared/Floofstation/Traits/Events/CummingDoAfterEvent.cs new file mode 100644 index 00000000000..418afed2185 --- /dev/null +++ b/Content.Shared/Floofstation/Traits/Events/CummingDoAfterEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.FloofStation.Traits.Events; + +[Serializable, NetSerializable] +public sealed partial class CummingDoAfterEvent : SimpleDoAfterEvent +{ +} + diff --git a/Content.Shared/Floofstation/Traits/Events/MilkingDoAfterEvent.cs b/Content.Shared/Floofstation/Traits/Events/MilkingDoAfterEvent.cs new file mode 100644 index 00000000000..e4c44b7ad0d --- /dev/null +++ b/Content.Shared/Floofstation/Traits/Events/MilkingDoAfterEvent.cs @@ -0,0 +1,9 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.FloofStation.Traits.Events; + +[Serializable, NetSerializable] +public sealed partial class MilkingDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Resources/Locale/en-US/floofstation/cum/cum-verb.ftl b/Resources/Locale/en-US/floofstation/cum/cum-verb.ftl new file mode 100644 index 00000000000..8bc69128de2 --- /dev/null +++ b/Resources/Locale/en-US/floofstation/cum/cum-verb.ftl @@ -0,0 +1,5 @@ +cum-verb-dry = Your cum tank is empty. +cum-verb-success = You fill {THE($target)} with {$amount}u of cum from your cock. +cum-verb-success-ground = You pump out some cum all over the ground! + +cum-verb-get-text = Cum diff --git a/Resources/Locale/en-US/floofstation/milk/milk-verb.ftl b/Resources/Locale/en-US/floofstation/milk/milk-verb.ftl new file mode 100644 index 00000000000..e26c0dec0aa --- /dev/null +++ b/Resources/Locale/en-US/floofstation/milk/milk-verb.ftl @@ -0,0 +1,5 @@ +milk-verb-dry = Your breasts are empty. +milk-verb-success = You fill {THE($target)} with {$amount}u of milk from your leaky breasts. +milk-verb-success-ground = You squirt out some milk all over the ground! + +milk-verb-get-text = Milk diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index d331cf273cb..93b46867692 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -75,3 +75,12 @@ trait-name-Foreigner = Foreigner trait-description-Foreigner = For one reason or another you do not speak this station's primary language. Instead, you have a translator issued to you that only you can use. + +trait-name-CumProducer = Cock +trait-description-CumProducer = You have a schlong between your legs. + +trait-name-MilkProducer = Boobs +trait-description-MilkProducer = You have a pair of large mammaries. + +trait-name-SquirtProducer = Pussy +trait-description-SquirtProducer = You have a slit between your legs. diff --git a/Resources/Prototypes/Floof/Reagents/natural_sauce.yml b/Resources/Prototypes/Floof/Reagents/natural_sauce.yml index 86c6984973f..fe089b2ac80 100644 --- a/Resources/Prototypes/Floof/Reagents/natural_sauce.yml +++ b/Resources/Prototypes/Floof/Reagents/natural_sauce.yml @@ -12,9 +12,9 @@ Drink: effects: - !type:SatiateThirst - factor: 1 + factor: 0.2 - !type:SatiateHunger - factor: 4 + factor: 1 footstepSound: collection: FootstepSticky params: diff --git a/Resources/Prototypes/FloofStation/Traits/lewd.yml b/Resources/Prototypes/FloofStation/Traits/lewd.yml new file mode 100644 index 00000000000..d3cd1304479 --- /dev/null +++ b/Resources/Prototypes/FloofStation/Traits/lewd.yml @@ -0,0 +1,60 @@ +- type: trait + id: CumProducer + category: Physical + requirements: + - !type:CharacterJobRequirement + inverted: true + jobs: + - Borg + - MedicalBorg + components: + - type: CumProducer + solutionname: "penis" + - type: SolutionContainerManager + solutions: + penis: + maxVol: 250 + reagents: + - ReagentId: Cum + Quantity: 30 + +- type: trait + id: MilkProducer + category: Physical + requirements: + - !type:CharacterJobRequirement + inverted: true + jobs: + - Borg + - MedicalBorg + components: + - type: MilkProducer + solutionname: "breasts" + - type: SolutionContainerManager + solutions: + breasts: + maxVol: 250 + reagents: + - ReagentId: Milk + Quantity: 30 + +# WIP - Needs a Reagent +# - type: trait +# id: SquirtProducer +# category: Physical +# requirements: +# - !type:CharacterJobRequirement +# inverted: true +# jobs: +# - Borg +# - MedicalBorg +# components: +# - type: SquirtProducer +# solutionname: "vagina" +# - type: SolutionContainerManager +# solutions: +# vagina: +# maxVol: 250 +# reagents: +# - ReagentId: Water +# Quantity: 30