diff --git a/Content.Shared/_Euphoria/Lewd/Milker/Components/MilkerComponent.cs b/Content.Shared/_Euphoria/Lewd/Milker/Components/MilkerComponent.cs new file mode 100644 index 00000000000..abfbae8b9fd --- /dev/null +++ b/Content.Shared/_Euphoria/Lewd/Milker/Components/MilkerComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.FixedPoint; +using Content.Shared.Whitelist; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; +using System.Numerics; + +namespace Content.Shared._Floof.Lewd.Milker; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MilkerComponent : Component +{ + [DataField, AutoNetworkedField] + public EntityUid? MilkedEntity; // Entity being milked + [DataField] + public EntityWhitelist MilkedEntityWhitelist; // Whitelist for valid milkable entities + [DataField] + public TimeSpan AttachDelay = TimeSpan.FromSeconds(5); // Delay to attach an entity + [DataField] + public string? MilkedSolution; // Solution of entity being milked + [DataField] + public string[] MilkedSolutionWhitelist = ["mammaryGlands", "udder"]; // Valid solutions for milking + [DataField] + public FixedPoint2 MilkedAmount = 2; // Amount to be milked + [DataField] + public string PopupDataset; // Dataset for "encouraging prompts" + [DataField] + public double PopupChance = 0.05; // Chance for a popup per update + [DataField] + public TimeSpan UpdateDelay = TimeSpan.FromSeconds(1); + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextUpdate = TimeSpan.FromSeconds(0); + [DataField] + public string Solution = "tank"; // Solution for fluids to be transfered too + [DataField] + public SpriteSpecifier TubeSprite = default!; // Sprite for the connection tube + [DataField] + public double TubeLength = 5; // Maximum length of the tube + [DataField] + public Vector2 TubeOffsetSource, TubeOffsetTarget = Vector2.Zero; + public const string VisualsContainerName = "tube-visuals"; +} diff --git a/Content.Shared/_Euphoria/Lewd/Milker/MilkerDoAfter.cs b/Content.Shared/_Euphoria/Lewd/Milker/MilkerDoAfter.cs new file mode 100644 index 00000000000..6e16903d569 --- /dev/null +++ b/Content.Shared/_Euphoria/Lewd/Milker/MilkerDoAfter.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared._Floof.Lewd.Milker; + +[Serializable, NetSerializable] +public sealed partial class MilkerDoAfterEvent : SimpleDoAfterEvent +{ + +} diff --git a/Content.Shared/_Euphoria/Lewd/Milker/MilkerSystem.cs b/Content.Shared/_Euphoria/Lewd/Milker/MilkerSystem.cs new file mode 100644 index 00000000000..11f9d4ea58d --- /dev/null +++ b/Content.Shared/_Euphoria/Lewd/Milker/MilkerSystem.cs @@ -0,0 +1,232 @@ +using Content.Shared.DragDrop; +using Content.Shared.Whitelist; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.ActionBlocker; +using Content.Shared.Interaction; +using Content.Shared.DoAfter; +using Content.Shared.Chemistry.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Popups; +using Content.Shared.Dataset; +using Content.Shared.Destructible; +using Robust.Shared.Timing; +using System.Numerics; +using Content.Shared.Power.EntitySystems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Content.Shared.Audio; +using Content.Shared.Random.Helpers; +using Robust.Shared.Containers; +using Content.Shared._Floof.Leash.Components; +using Robust.Shared.Network; +using System.Linq; + +namespace Content.Shared._Floof.Lewd.Milker; + +[Serializable] +public sealed class MilkerSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + [Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambient = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly INetManager _net = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnCanDrop); + SubscribeLocalEvent(OnDragDropped); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(FinishAttaching); + SubscribeLocalEvent(OnDestroyed); + } + + private void OnComponentInit(Entity entity, ref ComponentStartup args) + { + base.Initialize(); + } + + private void OnDestroyed(Entity entity, ref DestructionEventArgs args) + { + Detach(entity); + } + + private void OnCanDrop(Entity entity, ref CanDropTargetEvent args) + { + args.Handled = true; + args.CanDrop |= CheckInteraction(entity, args.Dragged, args.User); + } + + private void OnDragDropped(Entity entity, ref DragDropTargetEvent args) + { + args.Handled = true; + TryAttaching(entity, args.Dragged, args.User); + + } + + private void OnInteractHand(Entity entity, ref InteractHandEvent args) + { + TryAttaching(entity, args.User, args.User); + } + + public void TryAttaching(Entity entity, EntityUid target, EntityUid user) + { + if (!CheckInteraction(entity, target, user)) + return; + + if (entity.Comp.MilkedEntity != null) + { + Detach(entity); + } + else if (CheckMilkable(entity, target)) + { + var doAfterArgs = new DoAfterArgs(EntityManager, user, entity.Comp.AttachDelay, new MilkerDoAfterEvent(), entity, target) + { + BreakOnMove = true, + BreakOnDamage = true, + AttemptFrequency = AttemptFrequency.EveryTick + }; + _popupSystem.PopupPredicted(Loc.GetString("milker-attach-start-popup", + ("user", Identity.Entity(user, EntityManager)), + ("target", Identity.Entity(target, EntityManager)), + ("entity", Identity.Entity(entity, EntityManager)) + ), entity.Owner, user); + _doAfter.TryStartDoAfter(doAfterArgs); + } + else + { + // Only shown to the person who attempted to attach them + _popupSystem.PopupClient(Loc.GetString("milker-attach-fail-popup", + ("target", Identity.Entity(target, EntityManager)) + ), entity.Owner, user); + } + } + + void FinishAttaching(Entity entity, ref MilkerDoAfterEvent args) + { + if (!args.Cancelled && args.Target != null) + { + Attach(entity, (EntityUid)args.Target); + } + } + + public void Attach(Entity entity, EntityUid target) + { + _popupSystem.PopupPredicted(Loc.GetString("milker-attach-finish-popup", + ("target", Identity.Entity(target, EntityManager)), + ("entity", Identity.Entity(entity, EntityManager)) + ), entity.Owner, target); + _ambient.SetAmbience(entity, true); + entity.Comp.MilkedSolution = _solution.EnumerateSolutions(target).First((solution) => entity.Comp.MilkedSolutionWhitelist.Contains(solution.Name)).Name; + entity.Comp.MilkedEntity = target; + + if (entity.Comp.TubeSprite is { } sprite) + { + _container.EnsureContainer(entity, MilkerComponent.VisualsContainerName); + if (EntityManager.TrySpawnInContainer(null, entity, MilkerComponent.VisualsContainerName, out var visualEntity)) + { + var visualComp = EnsureComp(visualEntity.Value); + visualComp.Sprite = sprite; + visualComp.Source = entity; + visualComp.Target = target; + } + } + Dirty(entity); + } + + public void Detach(Entity entity) + { + if (entity.Comp.MilkedEntity != null) + { + _popupSystem.PopupPredicted(Loc.GetString("milker-detach-popup", + ("target", Identity.Entity((EntityUid)entity.Comp.MilkedEntity, EntityManager)), + ("entity", Identity.Entity(entity, EntityManager)) + ), entity.Owner, entity.Comp.MilkedEntity); + _ambient.SetAmbience(entity, false); + entity.Comp.MilkedSolution = null; + entity.Comp.MilkedEntity = null; + if (_container.TryGetContainer(entity, MilkerComponent.VisualsContainerName, out var visualsContainer)) + _container.CleanContainer(visualsContainer); + Dirty(entity); + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + // If not attached to anything, skip + if (component.MilkedEntity == null) + continue; + + // Check if it's time for our next tick + if (_timing.CurTime < component.NextUpdate) + continue; + component.NextUpdate = _timing.CurTime + component.UpdateDelay; + + // If the milked entity is too far away, detach them + if (Vector2.DistanceSquared( + _xform.GetWorldPosition(uid), + _xform.GetWorldPosition((EntityUid)component.MilkedEntity)) + > component.TubeLength * component.TubeLength) + { + Detach(new Entity(uid, component)); + continue; + } + + // Check if we have power (also should check if the power switch is on) + if (!_powerReceiver.IsPowered(uid)) + continue; + + // Do the milking + _solution.TryGetSolution(uid, component.Solution, out Entity? solution); + _solution.TryGetSolution((EntityUid)component.MilkedEntity, component.MilkedSolution, out Entity? source); + if (solution == null || source == null) + continue; + _solution.TryTransferSolution( + (Entity)solution, + ((SolutionComponent)source).Solution, + component.MilkedAmount); + + // Display some supportive prompts + _prototypeManager.TryIndex(component.PopupDataset, out LocalizedDatasetPrototype? dataset); + if (dataset != null && _random.NextFloat() < component.PopupChance) + _popupSystem.PopupClient(Loc.GetString(_random.Pick(dataset.Values)), (EntityUid)component.MilkedEntity, (EntityUid)component.MilkedEntity, PopupType.Medium); + } + } + + // Checks if the entity is whitelist valid - can we milk this twink? + public bool CheckMilkable(Entity milker, EntityUid target) + { + if (!_whitelist.IsWhitelistPass(milker.Comp.MilkedEntityWhitelist, target)) + return false; + if (!_solution.EnumerateSolutions(target).Any((solution) => milker.Comp.MilkedSolutionWhitelist.Contains(solution.Name))) + return false; + return true; + } + + // Checks if the entity can actually be reached by the milker and milkee + public bool CheckInteraction(Entity milker, EntityUid target, EntityUid user) + { + if (!_actionBlocker.CanComplexInteract(user)) + return false; + + if (!_interaction.InRangeUnobstructed(user, target)) + return false; + return true; + } + +} diff --git a/Resources/Locale/en-US/_Euphoria/lewd/milker.ftl b/Resources/Locale/en-US/_Euphoria/lewd/milker.ftl new file mode 100644 index 00000000000..35566ec43d3 --- /dev/null +++ b/Resources/Locale/en-US/_Euphoria/lewd/milker.ftl @@ -0,0 +1,22 @@ +milker-attach-start-popup = {THE($user)} is attaching {THE($target)} to {THE($entity)} +milker-attach-fail-popup = {THE($target)} can't be milked! +milker-attach-finish-popup = {THE($target)} is attached to {THE($entity)} + +milker-detach-popup = {THE($target)} is detached from {THE($entity)} + +milker-popup-1 = You feel the milker tug you +milker-popup-2 = You feel the pressure on your chest slowly relieved +milker-popup-3 = Moo +milker-popup-4 = Moo! +milker-popup-5 = Moo... +milker-popup-6 = Moo? +milker-popup-7 = The milker tugs your nipples +milker-popup-8 = The cups suckle your breasts +milker-popup-9 = You feel like a cow. Moo. +milker-popup-10 = You have the urge to moo +milker-popup-11 = The pressure in your breasts quickly turns to pleasure +milker-popup-12 = Your nipples are tingling +milker-popup-13 = Pump. Pump. Pump. +milker-popup-14 = You feel the suction in time with your heartbeat +milker-popup-15 = Your nipples feel numb +milker-popup-16 = Warmth radiates through your chest diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/service.yml b/Resources/Prototypes/Recipes/Lathes/Packs/service.yml index 71d88e5dbc0..541635cd12a 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/service.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/service.yml @@ -62,6 +62,7 @@ - type: latheRecipePack parent: - ServiceBoardsDeltaV # DeltaV + - ServiceBoardsEuphoria # Euphoria id: ServiceBoards recipes: - BiofabricatorMachineCircuitboard diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index 5e347d8e743..27b5a86fb33 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -14,6 +14,7 @@ - SeedExtractorMachineCircuitboard - HydroponicsTrayMachineCircuitboard - ReagentGrinderIndustrialMachineCircuitboard + - MilkingMachineCircuitboard # Euphoria - type: technology id: CritterMechs diff --git a/Resources/Prototypes/_Euphoria/Entities/Objects/Devices/Circuitboards/Machine/lewd.yml b/Resources/Prototypes/_Euphoria/Entities/Objects/Devices/Circuitboards/Machine/lewd.yml new file mode 100644 index 00000000000..08753114c8f --- /dev/null +++ b/Resources/Prototypes/_Euphoria/Entities/Objects/Devices/Circuitboards/Machine/lewd.yml @@ -0,0 +1,21 @@ +- type: entity + id: MilkingMachineCircuitboard + parent: BaseMachineCircuitboard + name: milking machine board + description: A machine printed circuit board for a milking machine. + components: + - type: Sprite + state: engineering + - type: MachineBoard + prototype: MilkingMachine + stackRequirements: + Plastic: 4 + Glass: 4 + componentRequirements: + GasVolumePump: + amount: 1 + defaultPrototype: GasVolumePump + tagRequirements: + Bucket: + amount: 2 + defaultPrototype: Bucket diff --git a/Resources/Prototypes/_Euphoria/Entities/Structures/Lewd/milker.yml b/Resources/Prototypes/_Euphoria/Entities/Structures/Lewd/milker.yml new file mode 100644 index 00000000000..55ec99c4177 --- /dev/null +++ b/Resources/Prototypes/_Euphoria/Entities/Structures/Lewd/milker.yml @@ -0,0 +1,55 @@ +- type: entity + parent: [ BaseMachinePowered, ConstructibleMachine, StorageTank ] + id: MilkingMachine + name: milking machine + description: An industrial grade milking machine - designed for cows. Doesn't say what kind of cows though. + placement: + mode: SnapgridCenter + components: + - type: Sprite + sprite: _Euphoria/Objects/Structures/Lewd/milker.rsi + layers: + - state: milktank-1 + - state: milktank + map: ["enum.SolutionContainerLayers.Fill"] + visible: false + - state: unlit + map: ["unlit"] + - type: Appearance + - type: Machine + board: MilkingMachineCircuitboard + - type: AmbientSound + enabled: false + sound: + path: /Audio/_Floof/Lewd/vibrate_loop.ogg + - type: GenericVisualizer + visuals: + enum.PowerDeviceVisuals.Powered: + unlit: + True: { visible: false } + False: { visible: true } + - type: PowerSwitch + - type: SolutionContainerVisuals + maxFillLevels: 7 + fillBaseName: milktank-2- + - type: ExaminableSolution + solution: tank + exactVolume: true + - type: Milker + tubeSprite: + sprite: _Euphoria/Objects/Structures/Lewd/milker_tube.rsi + state: tube + popupDataset: MilkerPopups + milkedAmount: 2 + milkedSolutionWhitelist: + - mammaryGlands + - udder + milkedEntityWhitelist: + components: + - MobState + +- type: localizedDataset + id: MilkerPopups + values: + prefix: milker-popup- + count: 16 diff --git a/Resources/Prototypes/_Euphoria/Recipes/Lathes/Packs/service.yml b/Resources/Prototypes/_Euphoria/Recipes/Lathes/Packs/service.yml new file mode 100644 index 00000000000..e77f7f8cc5a --- /dev/null +++ b/Resources/Prototypes/_Euphoria/Recipes/Lathes/Packs/service.yml @@ -0,0 +1,4 @@ +- type: latheRecipePack + id: ServiceBoardsEuphoria + recipes: + - MilkingMachineCircuitboard diff --git a/Resources/Prototypes/_Euphoria/Recipes/Lathes/machine_boards.yml b/Resources/Prototypes/_Euphoria/Recipes/Lathes/machine_boards.yml new file mode 100644 index 00000000000..6833bd1f93b --- /dev/null +++ b/Resources/Prototypes/_Euphoria/Recipes/Lathes/machine_boards.yml @@ -0,0 +1,5 @@ + +- type: latheRecipe + parent: [ BaseCircuitboardRecipe, BaseServiceMachineRecipeCategory ] + id: MilkingMachineCircuitboard + result: MilkingMachineCircuitboard diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/meta.json b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/meta.json new file mode 100644 index 00000000000..2d3ad080cae --- /dev/null +++ b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/meta.json @@ -0,0 +1,49 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "by WinstonThrasher (DocThrasher on newgrounds and bsky hi hello), based on the high capacity tanks taken from tgstation at commit https://github.com/tgstation/tgstation/commit/8442af39ee82b813194f71db82edd2923d97818d", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "milktank" + }, + { + "name": "unlit" + }, + { + "name": "milktank-1", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "milktank-2-1" + }, + { + "name": "milktank-2-2" + }, + { + "name": "milktank-2-3" + }, + { + "name": "milktank-2-4" + }, + { + "name": "milktank-2-5" + }, + { + "name": "milktank-2-6" + }, + { + "name": "milktank-2-7" + } + ] +} diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-1.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-1.png new file mode 100644 index 00000000000..1f8b54facbd Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-1.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-1.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-1.png new file mode 100644 index 00000000000..7ee4ba996cc Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-1.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-2.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-2.png new file mode 100644 index 00000000000..94399242b95 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-2.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-3.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-3.png new file mode 100644 index 00000000000..3a44e4c45c7 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-3.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-4.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-4.png new file mode 100644 index 00000000000..4bd1de4bb68 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-4.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-5.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-5.png new file mode 100644 index 00000000000..41a42d683d2 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-5.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-6.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-6.png new file mode 100644 index 00000000000..2da7e8de261 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-6.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-7.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-7.png new file mode 100644 index 00000000000..3d880f13222 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank-2-7.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank.png new file mode 100644 index 00000000000..ab762d06d22 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/milktank.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/unlit.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/unlit.png new file mode 100644 index 00000000000..f762ceb0482 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker.rsi/unlit.png differ diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker_tube.rsi/meta.json b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker_tube.rsi/meta.json new file mode 100644 index 00000000000..a7593957dd5 --- /dev/null +++ b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker_tube.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Compilatron (github) for SS14", + "size": { + "x": 8, + "y": 64 + }, + "states": [ + { + "name": "tube" + } + ] +} diff --git a/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker_tube.rsi/tube.png b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker_tube.rsi/tube.png new file mode 100644 index 00000000000..d6e452aa0f0 Binary files /dev/null and b/Resources/Textures/_Euphoria/Objects/Structures/Lewd/milker_tube.rsi/tube.png differ