Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 = ["breasts", "udder"];
[DataField]
public FixedPoint2 MilkedAmount = 10; // Amount to be milked
[DataField]
public string PopupDataset;
[DataField]
public double PopupChance = 0.25;
[DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(5);
[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";
}
10 changes: 10 additions & 0 deletions Content.Shared/_Euphoria/Lewd/Milker/MilkerDoAfter.cs
Original file line number Diff line number Diff line change
@@ -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
{

}
232 changes: 232 additions & 0 deletions Content.Shared/_Euphoria/Lewd/Milker/MilkerSystem.cs
Original file line number Diff line number Diff line change
@@ -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<MilkerComponent, ComponentStartup>(OnComponentInit);
SubscribeLocalEvent<MilkerComponent, CanDropTargetEvent>(OnCanDrop);
SubscribeLocalEvent<MilkerComponent, DragDropTargetEvent>(OnDragDropped);
SubscribeLocalEvent<MilkerComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<MilkerComponent, MilkerDoAfterEvent>(FinishAttaching);
SubscribeLocalEvent<MilkerComponent, DestructionEventArgs>(OnDestroyed);
}

private void OnComponentInit(Entity<MilkerComponent> entity, ref ComponentStartup args)
{
base.Initialize();
}

private void OnDestroyed(Entity<MilkerComponent> entity, ref DestructionEventArgs args)
{
Detach(entity);
}

private void OnCanDrop(Entity<MilkerComponent> entity, ref CanDropTargetEvent args)
{
args.Handled = true;
args.CanDrop |= CheckInteraction(entity, args.Dragged, args.User);
}

private void OnDragDropped(Entity<MilkerComponent> entity, ref DragDropTargetEvent args)
{
args.Handled = true;
TryAttaching(entity, args.Dragged, args.User);

}

private void OnInteractHand(Entity<MilkerComponent> entity, ref InteractHandEvent args)
{
TryAttaching(entity, args.User, args.User);
}

public void TryAttaching(Entity<MilkerComponent> 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<MilkerComponent> entity, ref MilkerDoAfterEvent args)
{
if (!args.Cancelled && args.Target != null)
{
Attach(entity, (EntityUid)args.Target);
}
}

public void Attach(Entity<MilkerComponent> 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<ContainerSlot>(entity, MilkerComponent.VisualsContainerName);
if (EntityManager.TrySpawnInContainer(null, entity, MilkerComponent.VisualsContainerName, out var visualEntity))
{
var visualComp = EnsureComp<LeashedVisualsComponent>(visualEntity.Value);
visualComp.Sprite = sprite;
visualComp.Source = entity;
visualComp.Target = target;
}
}
Dirty(entity);
}

public void Detach(Entity<MilkerComponent> 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<MilkerComponent>();
while (query.MoveNext(out var uid, out var component))
{
// If not attached to anything, skip
if (component.MilkedEntity == null)
continue;

// 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<MilkerComponent>(uid, component));
continue;
}

// Check if we have power (also should check if the power switch is on)
if (!_powerReceiver.IsPowered(uid))
continue;

// Check if it's time for our next tick
if (_timing.CurTime < component.NextUpdate)
continue;
component.NextUpdate = _timing.CurTime + component.UpdateDelay;
Comment on lines +189 to +192
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please move this to the beginning of the while loop body. The methods you're invoking before doing this check are way too expensive too run 60 times a second (300 times more than necessary) for every milking machine entity


// Do the milking
_solution.TryGetSolution(uid, component.Solution, out Entity<SolutionComponent>? solution);
_solution.TryGetSolution((EntityUid)component.MilkedEntity, component.MilkedSolution, out Entity<SolutionComponent>? source);
if (solution == null || source == null)
continue;
_solution.TryTransferSolution(
(Entity<SolutionComponent>)solution,
((SolutionComponent)source).Solution,
component.MilkedAmount);

// Display some supportive prompts
_prototypeManager.TryIndex<LocalizedDatasetPrototype>(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<MilkerComponent> 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<MilkerComponent> milker, EntityUid target, EntityUid user)
{
if (!_actionBlocker.CanComplexInteract(user))
return false;

if (!_interaction.InRangeUnobstructed(user, target))
return false;
return true;
}

}
22 changes: 22 additions & 0 deletions Resources/Locale/en-US/_Euphoria/lewd/milker.ftl
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Resources/Prototypes/Recipes/Lathes/Packs/service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
- type: latheRecipePack
parent:
- ServiceBoardsDeltaV # DeltaV
- ServiceBoardsEuphoria # Euphoria
id: ServiceBoards
recipes:
- BiofabricatorMachineCircuitboard
Expand Down
1 change: 1 addition & 0 deletions Resources/Prototypes/Research/civilianservices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- SeedExtractorMachineCircuitboard
- HydroponicsTrayMachineCircuitboard
- ReagentGrinderIndustrialMachineCircuitboard
- MilkingMachineCircuitboard # Euphoria

- type: technology
id: CritterMechs
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading